claudedesk 1.0.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 (182) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +431 -0
  3. package/config/repos.example.json +128 -0
  4. package/config/settings.example.json +64 -0
  5. package/config/skills/code-review.md +76 -0
  6. package/config/skills/full-check.md +26 -0
  7. package/config/skills/lint-fix.md +23 -0
  8. package/dist/api/agent-routes.d.ts +2 -0
  9. package/dist/api/agent-routes.d.ts.map +1 -0
  10. package/dist/api/agent-routes.js +251 -0
  11. package/dist/api/agent-routes.js.map +1 -0
  12. package/dist/api/app-routes.d.ts +2 -0
  13. package/dist/api/app-routes.d.ts.map +1 -0
  14. package/dist/api/app-routes.js +150 -0
  15. package/dist/api/app-routes.js.map +1 -0
  16. package/dist/api/docker-routes.d.ts +2 -0
  17. package/dist/api/docker-routes.d.ts.map +1 -0
  18. package/dist/api/docker-routes.js +167 -0
  19. package/dist/api/docker-routes.js.map +1 -0
  20. package/dist/api/middleware.d.ts +6 -0
  21. package/dist/api/middleware.d.ts.map +1 -0
  22. package/dist/api/middleware.js +293 -0
  23. package/dist/api/middleware.js.map +1 -0
  24. package/dist/api/pin-auth.d.ts +65 -0
  25. package/dist/api/pin-auth.d.ts.map +1 -0
  26. package/dist/api/pin-auth.js +218 -0
  27. package/dist/api/pin-auth.js.map +1 -0
  28. package/dist/api/routes.d.ts +2 -0
  29. package/dist/api/routes.d.ts.map +1 -0
  30. package/dist/api/routes.js +473 -0
  31. package/dist/api/routes.js.map +1 -0
  32. package/dist/api/settings-routes.d.ts +2 -0
  33. package/dist/api/settings-routes.d.ts.map +1 -0
  34. package/dist/api/settings-routes.js +570 -0
  35. package/dist/api/settings-routes.js.map +1 -0
  36. package/dist/api/skill-routes.d.ts +2 -0
  37. package/dist/api/skill-routes.d.ts.map +1 -0
  38. package/dist/api/skill-routes.js +88 -0
  39. package/dist/api/skill-routes.js.map +1 -0
  40. package/dist/api/terminal-routes.d.ts +2 -0
  41. package/dist/api/terminal-routes.d.ts.map +1 -0
  42. package/dist/api/terminal-routes.js +3524 -0
  43. package/dist/api/terminal-routes.js.map +1 -0
  44. package/dist/api/tunnel-routes.d.ts +2 -0
  45. package/dist/api/tunnel-routes.d.ts.map +1 -0
  46. package/dist/api/tunnel-routes.js +196 -0
  47. package/dist/api/tunnel-routes.js.map +1 -0
  48. package/dist/api/workspace-routes.d.ts +3 -0
  49. package/dist/api/workspace-routes.d.ts.map +1 -0
  50. package/dist/api/workspace-routes.js +649 -0
  51. package/dist/api/workspace-routes.js.map +1 -0
  52. package/dist/cli.d.ts +3 -0
  53. package/dist/cli.d.ts.map +1 -0
  54. package/dist/cli.js +276 -0
  55. package/dist/cli.js.map +1 -0
  56. package/dist/client/assets/index-B4r0njGe.js +780 -0
  57. package/dist/client/assets/index-CY_9MyE0.css +1 -0
  58. package/dist/client/favicon.svg +5 -0
  59. package/dist/client/icons/icon-192.svg +5 -0
  60. package/dist/client/icons/icon-512.svg +5 -0
  61. package/dist/client/icons/logo-with-message.png +0 -0
  62. package/dist/client/icons/logo.png +0 -0
  63. package/dist/client/index.html +25 -0
  64. package/dist/client/manifest.json +62 -0
  65. package/dist/client/sw.js +243 -0
  66. package/dist/config/agent-usage.d.ts +34 -0
  67. package/dist/config/agent-usage.d.ts.map +1 -0
  68. package/dist/config/agent-usage.js +87 -0
  69. package/dist/config/agent-usage.js.map +1 -0
  70. package/dist/config/repos.d.ts +34 -0
  71. package/dist/config/repos.d.ts.map +1 -0
  72. package/dist/config/repos.js +412 -0
  73. package/dist/config/repos.js.map +1 -0
  74. package/dist/config/settings.d.ts +634 -0
  75. package/dist/config/settings.d.ts.map +1 -0
  76. package/dist/config/settings.js +459 -0
  77. package/dist/config/settings.js.map +1 -0
  78. package/dist/config/skills.d.ts +18 -0
  79. package/dist/config/skills.d.ts.map +1 -0
  80. package/dist/config/skills.js +174 -0
  81. package/dist/config/skills.js.map +1 -0
  82. package/dist/config/workspaces.d.ts +961 -0
  83. package/dist/config/workspaces.d.ts.map +1 -0
  84. package/dist/config/workspaces.js +482 -0
  85. package/dist/config/workspaces.js.map +1 -0
  86. package/dist/core/app-manager.d.ts +85 -0
  87. package/dist/core/app-manager.d.ts.map +1 -0
  88. package/dist/core/app-manager.js +447 -0
  89. package/dist/core/app-manager.js.map +1 -0
  90. package/dist/core/claude-invoker.d.ts +49 -0
  91. package/dist/core/claude-invoker.d.ts.map +1 -0
  92. package/dist/core/claude-invoker.js +583 -0
  93. package/dist/core/claude-invoker.js.map +1 -0
  94. package/dist/core/claude-session-reader.d.ts +25 -0
  95. package/dist/core/claude-session-reader.d.ts.map +1 -0
  96. package/dist/core/claude-session-reader.js +184 -0
  97. package/dist/core/claude-session-reader.js.map +1 -0
  98. package/dist/core/claude-usage-query.d.ts +78 -0
  99. package/dist/core/claude-usage-query.d.ts.map +1 -0
  100. package/dist/core/claude-usage-query.js +294 -0
  101. package/dist/core/claude-usage-query.js.map +1 -0
  102. package/dist/core/git-credential-helper.d.ts +57 -0
  103. package/dist/core/git-credential-helper.d.ts.map +1 -0
  104. package/dist/core/git-credential-helper.js +176 -0
  105. package/dist/core/git-credential-helper.js.map +1 -0
  106. package/dist/core/git-sandbox.d.ts +135 -0
  107. package/dist/core/git-sandbox.d.ts.map +1 -0
  108. package/dist/core/git-sandbox.js +907 -0
  109. package/dist/core/git-sandbox.js.map +1 -0
  110. package/dist/core/github-integration.d.ts +66 -0
  111. package/dist/core/github-integration.d.ts.map +1 -0
  112. package/dist/core/github-integration.js +350 -0
  113. package/dist/core/github-integration.js.map +1 -0
  114. package/dist/core/github-oauth.d.ts +88 -0
  115. package/dist/core/github-oauth.d.ts.map +1 -0
  116. package/dist/core/github-oauth.js +244 -0
  117. package/dist/core/github-oauth.js.map +1 -0
  118. package/dist/core/gitlab-integration.d.ts +66 -0
  119. package/dist/core/gitlab-integration.d.ts.map +1 -0
  120. package/dist/core/gitlab-integration.js +353 -0
  121. package/dist/core/gitlab-integration.js.map +1 -0
  122. package/dist/core/gitlab-oauth.d.ts +100 -0
  123. package/dist/core/gitlab-oauth.d.ts.map +1 -0
  124. package/dist/core/gitlab-oauth.js +366 -0
  125. package/dist/core/gitlab-oauth.js.map +1 -0
  126. package/dist/core/insights-extractor.d.ts +68 -0
  127. package/dist/core/insights-extractor.d.ts.map +1 -0
  128. package/dist/core/insights-extractor.js +402 -0
  129. package/dist/core/insights-extractor.js.map +1 -0
  130. package/dist/core/logger.d.ts +27 -0
  131. package/dist/core/logger.d.ts.map +1 -0
  132. package/dist/core/logger.js +70 -0
  133. package/dist/core/logger.js.map +1 -0
  134. package/dist/core/process-runner.d.ts +27 -0
  135. package/dist/core/process-runner.d.ts.map +1 -0
  136. package/dist/core/process-runner.js +147 -0
  137. package/dist/core/process-runner.js.map +1 -0
  138. package/dist/core/project-detector.d.ts +30 -0
  139. package/dist/core/project-detector.d.ts.map +1 -0
  140. package/dist/core/project-detector.js +482 -0
  141. package/dist/core/project-detector.js.map +1 -0
  142. package/dist/core/qr-generator.d.ts +18 -0
  143. package/dist/core/qr-generator.d.ts.map +1 -0
  144. package/dist/core/qr-generator.js +61 -0
  145. package/dist/core/qr-generator.js.map +1 -0
  146. package/dist/core/remote-tunnel-manager.d.ts +59 -0
  147. package/dist/core/remote-tunnel-manager.d.ts.map +1 -0
  148. package/dist/core/remote-tunnel-manager.js +235 -0
  149. package/dist/core/remote-tunnel-manager.js.map +1 -0
  150. package/dist/core/shared-docker-manager.d.ts +41 -0
  151. package/dist/core/shared-docker-manager.d.ts.map +1 -0
  152. package/dist/core/shared-docker-manager.js +409 -0
  153. package/dist/core/shared-docker-manager.js.map +1 -0
  154. package/dist/core/skill-executor.d.ts +25 -0
  155. package/dist/core/skill-executor.d.ts.map +1 -0
  156. package/dist/core/skill-executor.js +171 -0
  157. package/dist/core/skill-executor.js.map +1 -0
  158. package/dist/core/terminal-session.d.ts +149 -0
  159. package/dist/core/terminal-session.d.ts.map +1 -0
  160. package/dist/core/terminal-session.js +2340 -0
  161. package/dist/core/terminal-session.js.map +1 -0
  162. package/dist/core/tunnel-manager.d.ts +35 -0
  163. package/dist/core/tunnel-manager.d.ts.map +1 -0
  164. package/dist/core/tunnel-manager.js +137 -0
  165. package/dist/core/tunnel-manager.js.map +1 -0
  166. package/dist/core/usage-manager.d.ts +57 -0
  167. package/dist/core/usage-manager.d.ts.map +1 -0
  168. package/dist/core/usage-manager.js +363 -0
  169. package/dist/core/usage-manager.js.map +1 -0
  170. package/dist/core/ws-manager.d.ts +39 -0
  171. package/dist/core/ws-manager.d.ts.map +1 -0
  172. package/dist/core/ws-manager.js +190 -0
  173. package/dist/core/ws-manager.js.map +1 -0
  174. package/dist/index.d.ts +7 -0
  175. package/dist/index.d.ts.map +1 -0
  176. package/dist/index.js +229 -0
  177. package/dist/index.js.map +1 -0
  178. package/dist/types.d.ts +868 -0
  179. package/dist/types.d.ts.map +1 -0
  180. package/dist/types.js +119 -0
  181. package/dist/types.js.map +1 -0
  182. package/package.json +96 -0
@@ -0,0 +1,907 @@
1
+ import { execSync } from 'child_process';
2
+ import { writeFileSync, mkdirSync, existsSync, unlinkSync, readdirSync, statSync, rmSync, readFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ // Windows reserved device names that cannot be used as filenames
5
+ // These cause git add to fail on Windows
6
+ const WINDOWS_RESERVED_NAMES = new Set([
7
+ 'con', 'prn', 'aux', 'nul',
8
+ 'com1', 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 'com8', 'com9',
9
+ 'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9',
10
+ ]);
11
+ /**
12
+ * Remove Windows reserved device name files from a directory (recursively)
13
+ * These files cannot be added to git on Windows
14
+ */
15
+ function removeWindowsReservedFiles(dirPath) {
16
+ if (!existsSync(dirPath))
17
+ return;
18
+ try {
19
+ const entries = readdirSync(dirPath);
20
+ for (const entry of entries) {
21
+ const fullPath = join(dirPath, entry);
22
+ // Check if this is a reserved name (case-insensitive, with or without extension)
23
+ const baseName = entry.split('.')[0].toLowerCase();
24
+ if (WINDOWS_RESERVED_NAMES.has(baseName)) {
25
+ console.log(`[GitSandbox] Removing Windows reserved file: ${fullPath}`);
26
+ try {
27
+ unlinkSync(fullPath);
28
+ }
29
+ catch (e) {
30
+ console.warn(`[GitSandbox] Could not remove ${fullPath}: ${e instanceof Error ? e.message : e}`);
31
+ }
32
+ continue;
33
+ }
34
+ // Recurse into directories (but skip .git)
35
+ try {
36
+ const stat = statSync(fullPath);
37
+ if (stat.isDirectory() && entry !== '.git') {
38
+ removeWindowsReservedFiles(fullPath);
39
+ }
40
+ }
41
+ catch {
42
+ // statSync might fail on special files, skip them
43
+ }
44
+ }
45
+ }
46
+ catch (e) {
47
+ console.warn(`[GitSandbox] Could not scan directory ${dirPath}: ${e instanceof Error ? e.message : e}`);
48
+ }
49
+ }
50
+ // Common filler words to remove for more concise branch names
51
+ const FILLER_WORDS = new Set([
52
+ 'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
53
+ 'of', 'with', 'by', 'from', 'as', 'is', 'was', 'are', 'were', 'been',
54
+ 'be', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would',
55
+ 'could', 'should', 'may', 'might', 'must', 'shall', 'can', 'need',
56
+ 'that', 'this', 'these', 'those', 'i', 'you', 'we', 'they', 'it',
57
+ 'please', 'make', 'sure', 'also', 'just', 'only', 'into', 'onto',
58
+ ]);
59
+ /**
60
+ * Generate a slug from text for use in branch names
61
+ * e.g., "Add a login feature to the app" -> "add-login-feature-app"
62
+ * Removes filler words and limits to ~30 chars / 5 words max
63
+ */
64
+ function slugify(text, maxWords = 5, maxLength = 30) {
65
+ const words = text
66
+ .toLowerCase()
67
+ .replace(/[^a-z0-9\s]/g, '') // Remove special chars
68
+ .split(/\s+/) // Split into words
69
+ .filter(word => word.length > 0 && !FILLER_WORDS.has(word));
70
+ // Take first N meaningful words
71
+ const slug = words
72
+ .slice(0, maxWords)
73
+ .join('-');
74
+ // Limit total length and clean up
75
+ return slug
76
+ .slice(0, maxLength)
77
+ .replace(/-$/, ''); // Remove trailing dash after slice
78
+ }
79
+ /**
80
+ * Generate a branch name based on options
81
+ * Format: <prefix>/<summary> (e.g., claudedesk/fix-login-error)
82
+ * Falls back to <prefix>/<jobId-short> if no summary provided
83
+ */
84
+ export function generateBranchName(options) {
85
+ const { prefix, summary, jobId } = options;
86
+ // Generate summary slug or use short job ID as fallback
87
+ const summarySlug = summary ? slugify(summary) : jobId.slice(0, 8);
88
+ // Use configured prefix, or default to 'claudedesk'
89
+ const branchPrefix = prefix || 'claudedesk';
90
+ return `${branchPrefix}/${summarySlug}`;
91
+ }
92
+ export class GitSandbox {
93
+ exec(command, cwd, timeoutMs = 30000) {
94
+ try {
95
+ return execSync(command, {
96
+ cwd,
97
+ encoding: 'utf-8',
98
+ stdio: ['pipe', 'pipe', 'pipe'],
99
+ timeout: timeoutMs,
100
+ }).trim();
101
+ }
102
+ catch (error) {
103
+ const err = error;
104
+ if (err.killed) {
105
+ throw new Error(`Git command timed out: ${command}`);
106
+ }
107
+ throw new Error(`Git command failed: ${command}\n${err.stderr?.toString() || err.message}`);
108
+ }
109
+ }
110
+ // Check if working directory is clean (ignoring claudedesk-specific files)
111
+ isClean(repoPath) {
112
+ const status = this.exec('git status --porcelain', repoPath);
113
+ if (status.length === 0)
114
+ return true;
115
+ // Files that ClaudeDesk can safely ignore
116
+ const ignoredFiles = ['CLAUDE.md', 'claude.md', '.claude', '.claudedesk'];
117
+ const lines = status.split('\n').filter(Boolean);
118
+ const significantChanges = lines.filter((line) => {
119
+ // Extract filename from status line (format: "XY filename" or "XY -> filename")
120
+ const filename = line.slice(3).split(' -> ').pop()?.trim() || '';
121
+ return !ignoredFiles.some((ignored) => filename === ignored || filename.endsWith(`/${ignored}`));
122
+ });
123
+ return significantChanges.length === 0;
124
+ }
125
+ // Get current branch name
126
+ getCurrentBranch(repoPath) {
127
+ return this.exec('git branch --show-current', repoPath);
128
+ }
129
+ // Create a sandbox branch for the job
130
+ // skipCleanCheck: set to true for retries to preserve partial work from failed jobs
131
+ createSandbox(repoPath, branchName, skipCleanCheck = false) {
132
+ console.log(`[GitSandbox] Creating sandbox branch "${branchName}" in ${repoPath}`);
133
+ // Check if repo is clean (ignoring claudedesk files like CLAUDE.md)
134
+ // Skip this check for retries to preserve partial work
135
+ if (!skipCleanCheck) {
136
+ console.log('[GitSandbox] Checking if repo is clean...');
137
+ if (!this.isClean(repoPath)) {
138
+ throw new Error('Working directory is not clean. Please commit or stash changes before running a job.');
139
+ }
140
+ console.log('[GitSandbox] Repo is clean');
141
+ }
142
+ else {
143
+ console.log('[GitSandbox] Skipping clean check (retry mode - preserving partial work)');
144
+ }
145
+ console.log('[GitSandbox] Getting current branch...');
146
+ const previousBranch = this.getCurrentBranch(repoPath);
147
+ // Checkout main/master branch first
148
+ let mainBranch = 'main';
149
+ console.log('[GitSandbox] Checking out main branch...');
150
+ try {
151
+ this.exec('git checkout main', repoPath);
152
+ console.log('[GitSandbox] Checked out main');
153
+ }
154
+ catch {
155
+ // Try master if main doesn't exist
156
+ console.log('[GitSandbox] main not found, trying master...');
157
+ try {
158
+ this.exec('git checkout master', repoPath);
159
+ mainBranch = 'master';
160
+ console.log('[GitSandbox] Checked out master');
161
+ }
162
+ catch {
163
+ // Stay on current branch if neither exists
164
+ mainBranch = previousBranch;
165
+ console.log(`[GitSandbox] Staying on ${previousBranch}`);
166
+ }
167
+ }
168
+ // Pull latest changes from remote (if remote exists)
169
+ // Use short timeout - if it hangs (e.g., waiting for credentials), skip it
170
+ console.log(`[GitSandbox] Pulling from origin ${mainBranch} (10s timeout)...`);
171
+ try {
172
+ this.exec(`git pull origin ${mainBranch}`, repoPath, 10000);
173
+ console.log('[GitSandbox] Pull completed');
174
+ }
175
+ catch (e) {
176
+ // Ignore pull errors (might not have remote, or timed out waiting for credentials)
177
+ console.log(`[GitSandbox] Pull skipped: ${e instanceof Error ? e.message : e}`);
178
+ }
179
+ // Create new sandbox branch
180
+ console.log(`[GitSandbox] Creating branch ${branchName}...`);
181
+ this.exec(`git checkout -b ${branchName}`, repoPath);
182
+ console.log(`[GitSandbox] Branch created`);
183
+ return {
184
+ branch: branchName,
185
+ previousBranch,
186
+ clean: true,
187
+ };
188
+ }
189
+ // Checkout an existing branch (for job retry - doesn't create new or pull from main)
190
+ checkoutExistingBranch(repoPath, branch) {
191
+ console.log(`[GitSandbox] Checking out existing branch "${branch}" in ${repoPath}`);
192
+ this.exec(`git checkout ${branch}`, repoPath);
193
+ console.log(`[GitSandbox] Checked out ${branch}`);
194
+ }
195
+ // Check if a branch exists in the repo
196
+ branchExists(repoPath, branch) {
197
+ try {
198
+ this.exec(`git rev-parse --verify ${branch}`, repoPath);
199
+ return true;
200
+ }
201
+ catch {
202
+ return false;
203
+ }
204
+ }
205
+ // Reset working directory and remove untracked files
206
+ resetAndClean(repoPath) {
207
+ console.log(`[GitSandbox] Resetting and cleaning working directory`);
208
+ this.exec('git reset --hard', repoPath);
209
+ try {
210
+ this.exec('git clean -fd', repoPath);
211
+ }
212
+ catch (e) {
213
+ // git clean can fail on Windows with reserved device names (nul, con, prn, etc.)
214
+ console.warn(`[GitSandbox] git clean failed (non-fatal): ${e instanceof Error ? e.message : e}`);
215
+ }
216
+ }
217
+ // Check if there are any uncommitted changes (for detecting if Claude made changes)
218
+ hasChanges(repoPath) {
219
+ const status = this.exec('git status --porcelain', repoPath);
220
+ return status.length > 0;
221
+ }
222
+ // Get list of changed files
223
+ getChangedFiles(repoPath) {
224
+ const status = this.exec('git status --porcelain', repoPath);
225
+ if (!status)
226
+ return [];
227
+ return status.split('\n').filter(Boolean).map(line => {
228
+ // Extract filename from status line (format: "XY filename" or "XY -> filename")
229
+ return line.slice(3).split(' -> ').pop()?.trim() || '';
230
+ }).filter(Boolean);
231
+ }
232
+ // Capture diff and changed files
233
+ captureDiff(repoPath, artifactsPath) {
234
+ const patch = this.exec('git diff', repoPath);
235
+ const stagedPatch = this.exec('git diff --cached', repoPath);
236
+ const fullPatch = patch + (stagedPatch ? '\n' + stagedPatch : '');
237
+ const changedFilesOutput = this.exec('git diff --name-only', repoPath);
238
+ const stagedFilesOutput = this.exec('git diff --cached --name-only', repoPath);
239
+ const changedFiles = [...new Set([
240
+ ...changedFilesOutput.split('\n').filter(Boolean),
241
+ ...stagedFilesOutput.split('\n').filter(Boolean),
242
+ ])];
243
+ // Save to artifacts
244
+ const gitDir = join(artifactsPath, 'git');
245
+ if (!existsSync(gitDir)) {
246
+ mkdirSync(gitDir, { recursive: true });
247
+ }
248
+ writeFileSync(join(gitDir, 'diff.patch'), fullPatch);
249
+ writeFileSync(join(gitDir, 'changed-files.json'), JSON.stringify(changedFiles, null, 2));
250
+ return { patch: fullPatch, changedFiles };
251
+ }
252
+ /**
253
+ * Generate a patch file for all changes on a branch compared to main
254
+ * This includes both committed and uncommitted changes
255
+ */
256
+ generatePatch(repoPath, branch) {
257
+ const mainBranch = this.getMainBranch(repoPath);
258
+ const targetBranch = branch || this.getCurrentBranch(repoPath);
259
+ // Get all committed changes on the branch compared to main
260
+ let patch = '';
261
+ try {
262
+ patch = this.exec(`git diff ${mainBranch}...${targetBranch}`, repoPath);
263
+ }
264
+ catch {
265
+ // If branch comparison fails, just get the current diff
266
+ patch = this.exec('git diff HEAD', repoPath);
267
+ }
268
+ // Also include any uncommitted changes
269
+ const uncommitted = this.exec('git diff', repoPath);
270
+ const staged = this.exec('git diff --cached', repoPath);
271
+ if (uncommitted) {
272
+ patch += '\n# Uncommitted changes:\n' + uncommitted;
273
+ }
274
+ if (staged) {
275
+ patch += '\n# Staged changes:\n' + staged;
276
+ }
277
+ return patch;
278
+ }
279
+ // Check if a remote exists
280
+ hasRemote(repoPath, remoteName = 'origin') {
281
+ try {
282
+ const remotes = this.exec('git remote', repoPath);
283
+ return remotes.split('\n').includes(remoteName);
284
+ }
285
+ catch {
286
+ return false;
287
+ }
288
+ }
289
+ // Get the main branch name (main or master)
290
+ getMainBranch(repoPath) {
291
+ try {
292
+ this.exec('git rev-parse --verify main', repoPath);
293
+ return 'main';
294
+ }
295
+ catch {
296
+ return 'master';
297
+ }
298
+ }
299
+ // Check if a branch can be merged cleanly into main (no conflicts)
300
+ canMergeCleanly(repoPath, branch) {
301
+ const mainBranch = this.getMainBranch(repoPath);
302
+ const currentBranch = this.getCurrentBranch(repoPath);
303
+ try {
304
+ // Make sure we're on main branch
305
+ if (currentBranch !== mainBranch) {
306
+ this.exec(`git checkout ${mainBranch}`, repoPath);
307
+ }
308
+ // Try a dry-run merge
309
+ this.exec(`git merge --no-commit --no-ff ${branch}`, repoPath);
310
+ // If we get here, merge is clean - abort it
311
+ this.exec('git merge --abort', repoPath);
312
+ return { canMerge: true };
313
+ }
314
+ catch (e) {
315
+ // Merge failed - there are conflicts
316
+ // Try to get the list of conflicting files
317
+ let conflictFiles = [];
318
+ try {
319
+ const status = this.exec('git diff --name-only --diff-filter=U', repoPath);
320
+ conflictFiles = status.split('\n').filter(Boolean);
321
+ }
322
+ catch {
323
+ // Ignore - we already know there's a conflict
324
+ }
325
+ // Abort the failed merge
326
+ try {
327
+ this.exec('git merge --abort', repoPath);
328
+ }
329
+ catch {
330
+ // Reset if abort fails
331
+ try {
332
+ this.exec('git reset --hard HEAD', repoPath);
333
+ }
334
+ catch {
335
+ // Ignore
336
+ }
337
+ }
338
+ // Restore original branch
339
+ if (currentBranch !== mainBranch) {
340
+ try {
341
+ this.exec(`git checkout ${currentBranch}`, repoPath);
342
+ }
343
+ catch {
344
+ // Ignore
345
+ }
346
+ }
347
+ return { canMerge: false, conflictFiles };
348
+ }
349
+ }
350
+ // Finalize the sandbox branch - commit changes and optionally push to origin
351
+ // Returns { pushed: boolean } indicating whether changes were pushed to remote
352
+ push(repoPath, branch, commitMessage) {
353
+ const currentBranch = this.getCurrentBranch(repoPath);
354
+ // Check if target branch exists - for create-repo workflows, the branch may not exist
355
+ // In that case, push the current branch instead
356
+ let branchToPush = branch;
357
+ if (currentBranch !== branch) {
358
+ if (this.branchExists(repoPath, branch)) {
359
+ this.exec(`git checkout ${branch}`, repoPath);
360
+ }
361
+ else {
362
+ console.log(`[GitSandbox] Branch ${branch} does not exist, pushing current branch ${currentBranch} instead`);
363
+ branchToPush = currentBranch;
364
+ }
365
+ }
366
+ // Remove Windows reserved device name files before staging (they cause git add to fail)
367
+ removeWindowsReservedFiles(repoPath);
368
+ // Stage all changes
369
+ this.exec('git add -A', repoPath);
370
+ // Check if there are changes to commit
371
+ if (!this.isClean(repoPath)) {
372
+ const message = commitMessage || 'ClaudeDesk: automated changes';
373
+ // Escape double quotes in commit message
374
+ const escapedMessage = message.replace(/"/g, '\\"');
375
+ this.exec(`git commit -m "${escapedMessage}"`, repoPath);
376
+ console.log(`[GitSandbox] Changes committed to branch ${branchToPush}`);
377
+ }
378
+ // Check if origin remote exists before pushing
379
+ if (this.hasRemote(repoPath, 'origin')) {
380
+ console.log(`[GitSandbox] Pushing branch ${branchToPush} to origin...`);
381
+ this.exec(`git push -u origin ${branchToPush}`, repoPath, 60000); // 60s timeout for push
382
+ console.log(`[GitSandbox] Branch pushed successfully`);
383
+ return { pushed: true };
384
+ }
385
+ else {
386
+ console.log(`[GitSandbox] No origin remote configured, skipping push`);
387
+ return { pushed: false };
388
+ }
389
+ }
390
+ // Discard the sandbox branch
391
+ discard(repoPath, branch, previousBranch) {
392
+ // Reset any changes
393
+ this.exec('git reset --hard', repoPath);
394
+ try {
395
+ this.exec('git clean -fd', repoPath);
396
+ }
397
+ catch (e) {
398
+ // git clean can fail on Windows with reserved device names (nul, con, prn, etc.)
399
+ console.warn(`[GitSandbox] git clean failed (non-fatal): ${e instanceof Error ? e.message : e}`);
400
+ }
401
+ // Checkout previous branch (non-fatal - just need to not be on the branch we're deleting)
402
+ try {
403
+ if (previousBranch && previousBranch !== branch) {
404
+ this.exec(`git checkout ${previousBranch}`, repoPath);
405
+ }
406
+ else {
407
+ try {
408
+ this.exec('git checkout main', repoPath);
409
+ }
410
+ catch {
411
+ this.exec('git checkout master', repoPath);
412
+ }
413
+ }
414
+ }
415
+ catch (e) {
416
+ // If we can't checkout another branch, that's okay - branch deletion might still work
417
+ console.warn(`[GitSandbox] checkout failed (non-fatal): ${e instanceof Error ? e.message : e}`);
418
+ }
419
+ // Delete the sandbox branch
420
+ try {
421
+ this.exec(`git branch -D ${branch}`, repoPath);
422
+ }
423
+ catch {
424
+ // Branch might not exist, ignore
425
+ }
426
+ }
427
+ // Initialize a new repo
428
+ init(repoPath) {
429
+ if (!existsSync(repoPath)) {
430
+ mkdirSync(repoPath, { recursive: true });
431
+ }
432
+ this.exec('git init', repoPath);
433
+ this.exec('git checkout -b main', repoPath);
434
+ }
435
+ // =========== WORKTREE METHODS ===========
436
+ /**
437
+ * Create a new worktree for isolated job execution
438
+ * @param repoPath - Path to the main repository
439
+ * @param worktreePath - Path where the worktree should be created
440
+ * @param branchName - Name of the branch to create
441
+ * @returns WorktreeResult with paths and branch info
442
+ */
443
+ createWorktree(repoPath, worktreePath, branchName) {
444
+ console.log(`[GitSandbox] Creating worktree at "${worktreePath}" from ${repoPath}`);
445
+ // First, ensure we're on main/master and up to date in the main repo
446
+ let mainBranch = 'main';
447
+ try {
448
+ this.exec('git checkout main', repoPath);
449
+ }
450
+ catch {
451
+ try {
452
+ this.exec('git checkout master', repoPath);
453
+ mainBranch = 'master';
454
+ }
455
+ catch {
456
+ // Stay on current branch if neither exists
457
+ mainBranch = this.getCurrentBranch(repoPath);
458
+ }
459
+ }
460
+ // Pull latest changes from remote (if remote exists)
461
+ // Use short timeout - if it hangs (e.g., waiting for credentials), skip it
462
+ console.log(`[GitSandbox] Pulling from origin ${mainBranch} (10s timeout)...`);
463
+ try {
464
+ this.exec(`git pull origin ${mainBranch}`, repoPath, 10000);
465
+ console.log('[GitSandbox] Pull completed');
466
+ }
467
+ catch (e) {
468
+ console.log(`[GitSandbox] Pull skipped: ${e instanceof Error ? e.message : e}`);
469
+ }
470
+ // Clean up stale worktree entries (directories that no longer exist)
471
+ // This is necessary before deleting branches that were checked out in those worktrees
472
+ try {
473
+ this.exec('git worktree prune', repoPath);
474
+ }
475
+ catch (e) {
476
+ console.warn(`[GitSandbox] Worktree prune failed: ${e instanceof Error ? e.message : e}`);
477
+ }
478
+ // Check if worktree directory already exists and remove it
479
+ if (existsSync(worktreePath)) {
480
+ console.log(`[GitSandbox] Worktree path ${worktreePath} already exists, removing...`);
481
+ try {
482
+ this.exec(`git worktree remove "${worktreePath}" --force`, repoPath);
483
+ }
484
+ catch (e) {
485
+ console.warn(`[GitSandbox] Failed to remove existing worktree via git: ${e instanceof Error ? e.message : e}`);
486
+ }
487
+ // Prune again after removal
488
+ try {
489
+ this.exec('git worktree prune', repoPath);
490
+ }
491
+ catch (e) {
492
+ console.warn(`[GitSandbox] Worktree prune failed: ${e instanceof Error ? e.message : e}`);
493
+ }
494
+ // If directory still exists, force remove it manually
495
+ if (existsSync(worktreePath)) {
496
+ console.log(`[GitSandbox] Directory still exists, removing manually...`);
497
+ try {
498
+ rmSync(worktreePath, { recursive: true, force: true });
499
+ }
500
+ catch (e) {
501
+ console.warn(`[GitSandbox] Manual removal failed: ${e instanceof Error ? e.message : e}`);
502
+ }
503
+ }
504
+ }
505
+ // Clean up orphaned branch from previous failed attempt if it exists
506
+ if (this.branchExists(repoPath, branchName)) {
507
+ console.log(`[GitSandbox] Branch ${branchName} already exists (orphaned from previous attempt), deleting it`);
508
+ try {
509
+ this.exec(`git branch -D "${branchName}"`, repoPath);
510
+ }
511
+ catch (e) {
512
+ // If branch deletion fails, it might still be locked by a worktree reference
513
+ // Try a more aggressive cleanup
514
+ console.warn(`[GitSandbox] Failed to delete orphaned branch, trying aggressive cleanup: ${e instanceof Error ? e.message : e}`);
515
+ try {
516
+ // Force prune all worktrees
517
+ this.exec('git worktree prune --verbose', repoPath);
518
+ // Try deletion again
519
+ this.exec(`git branch -D "${branchName}"`, repoPath);
520
+ console.log(`[GitSandbox] Branch ${branchName} deleted after aggressive cleanup`);
521
+ }
522
+ catch (e2) {
523
+ console.warn(`[GitSandbox] Aggressive cleanup failed: ${e2 instanceof Error ? e2.message : e2}`);
524
+ // As a last resort, create worktree without -b flag if branch exists
525
+ // This will use the existing branch instead of creating a new one
526
+ console.log(`[GitSandbox] Will attempt to use existing branch...`);
527
+ }
528
+ }
529
+ }
530
+ // Create worktree with new branch
531
+ // git worktree add -b <branch> <path>
532
+ console.log(`[GitSandbox] Creating worktree with branch ${branchName}...`);
533
+ // Check if branch still exists (cleanup might have failed)
534
+ if (this.branchExists(repoPath, branchName)) {
535
+ // Branch exists, create worktree using existing branch (without -b flag)
536
+ console.log(`[GitSandbox] Branch ${branchName} still exists, creating worktree with existing branch`);
537
+ this.exec(`git worktree add "${worktreePath}" "${branchName}"`, repoPath);
538
+ }
539
+ else {
540
+ // Normal case: create worktree with new branch
541
+ this.exec(`git worktree add -b "${branchName}" "${worktreePath}"`, repoPath);
542
+ }
543
+ console.log(`[GitSandbox] Worktree created at ${worktreePath}`);
544
+ return {
545
+ worktreePath,
546
+ branch: branchName,
547
+ repoPath,
548
+ };
549
+ }
550
+ /**
551
+ * Remove a worktree and optionally its branch
552
+ * @param repoPath - Path to the main repository
553
+ * @param worktreePath - Path to the worktree to remove
554
+ * @param deleteBranch - Branch name to delete (optional)
555
+ */
556
+ removeWorktree(repoPath, worktreePath, deleteBranch) {
557
+ console.log(`[GitSandbox] Removing worktree at "${worktreePath}"`);
558
+ // Remove the worktree
559
+ try {
560
+ this.exec(`git worktree remove "${worktreePath}" --force`, repoPath);
561
+ console.log('[GitSandbox] Worktree removed');
562
+ }
563
+ catch (e) {
564
+ // If worktree remove fails, try manual cleanup with prune
565
+ console.log(`[GitSandbox] Worktree remove failed, pruning: ${e instanceof Error ? e.message : e}`);
566
+ try {
567
+ this.exec('git worktree prune', repoPath);
568
+ }
569
+ catch {
570
+ // Prune might fail too, that's okay
571
+ }
572
+ }
573
+ // Delete the branch if requested
574
+ if (deleteBranch) {
575
+ console.log(`[GitSandbox] Deleting branch ${deleteBranch}`);
576
+ try {
577
+ this.exec(`git branch -D "${deleteBranch}"`, repoPath);
578
+ console.log('[GitSandbox] Branch deleted');
579
+ }
580
+ catch {
581
+ // Branch might not exist, that's okay
582
+ console.log('[GitSandbox] Branch deletion skipped (may not exist)');
583
+ }
584
+ }
585
+ }
586
+ /**
587
+ * Check if a worktree exists at the given path
588
+ */
589
+ worktreeExists(repoPath, worktreePath) {
590
+ try {
591
+ const output = this.exec('git worktree list --porcelain', repoPath);
592
+ // Normalize path separators for comparison
593
+ const normalizedWorktreePath = worktreePath.replace(/\\/g, '/');
594
+ return output.split('\n').some(line => {
595
+ if (line.startsWith('worktree ')) {
596
+ const path = line.substring(9).replace(/\\/g, '/');
597
+ return path === normalizedWorktreePath;
598
+ }
599
+ return false;
600
+ });
601
+ }
602
+ catch {
603
+ return false;
604
+ }
605
+ }
606
+ /**
607
+ * Check if a directory is a valid git worktree
608
+ * A valid worktree has a .git file (not directory) that points to the main repo's .git
609
+ * This is useful for detecting corrupted worktrees that exist as directories but aren't valid
610
+ */
611
+ isValidWorktree(worktreePath) {
612
+ try {
613
+ // Worktrees have a .git FILE (not directory) that contains "gitdir: ..."
614
+ const gitPath = join(worktreePath, '.git');
615
+ if (!existsSync(gitPath)) {
616
+ return false;
617
+ }
618
+ // Check if .git is a file (worktree) not a directory (regular repo)
619
+ const stat = statSync(gitPath);
620
+ if (stat.isDirectory()) {
621
+ // This is a regular git repo, not a worktree
622
+ return false;
623
+ }
624
+ // Verify it's a valid git worktree by running a git command
625
+ this.exec('git rev-parse --git-dir', worktreePath);
626
+ return true;
627
+ }
628
+ catch {
629
+ return false;
630
+ }
631
+ }
632
+ /**
633
+ * List all worktrees for a repository
634
+ */
635
+ listWorktrees(repoPath) {
636
+ try {
637
+ const output = this.exec('git worktree list --porcelain', repoPath);
638
+ const paths = [];
639
+ for (const line of output.split('\n')) {
640
+ if (line.startsWith('worktree ')) {
641
+ paths.push(line.substring(9));
642
+ }
643
+ }
644
+ return paths;
645
+ }
646
+ catch {
647
+ return [];
648
+ }
649
+ }
650
+ /**
651
+ * Push changes from a worktree
652
+ * @param worktreePath - Path to the worktree
653
+ * @param repoPath - Path to the main repository (for remote check)
654
+ * @param branch - Branch name to push
655
+ * @param commitMessage - Commit message
656
+ * @returns Object indicating what action was taken and any conflicts
657
+ */
658
+ pushWorktree(worktreePath, repoPath, branch, commitMessage) {
659
+ console.log(`[GitSandbox] Pushing from worktree at ${worktreePath}`);
660
+ // Remove Windows reserved device name files before staging (they cause git add to fail)
661
+ removeWindowsReservedFiles(worktreePath);
662
+ // Stage all changes
663
+ this.exec('git add -A', worktreePath);
664
+ // Check if there are changes to commit
665
+ if (!this.isClean(worktreePath)) {
666
+ const message = commitMessage || 'ClaudeDesk: automated changes';
667
+ const escapedMessage = message.replace(/"/g, '\\"');
668
+ this.exec(`git commit -m "${escapedMessage}"`, worktreePath);
669
+ console.log(`[GitSandbox] Changes committed to branch ${branch}`);
670
+ }
671
+ // Check if origin remote exists
672
+ if (this.hasRemote(repoPath, 'origin')) {
673
+ // HAS REMOTE: Push feature branch to origin (for PR workflow)
674
+ console.log(`[GitSandbox] Pushing branch ${branch} to origin...`);
675
+ try {
676
+ this.exec(`git push -u origin ${branch}`, worktreePath, 60000); // 60s timeout for push
677
+ console.log(`[GitSandbox] Branch pushed successfully`);
678
+ return { pushed: true, merged: false, conflict: false };
679
+ }
680
+ catch (e) {
681
+ console.log(`[GitSandbox] Push failed: ${e instanceof Error ? e.message : e}`);
682
+ return { pushed: false, merged: false, conflict: false };
683
+ }
684
+ }
685
+ else {
686
+ // NO REMOTE: Merge feature branch into main locally
687
+ console.log(`[GitSandbox] No origin remote configured, merging to main locally...`);
688
+ // First, check if merge will be clean
689
+ console.log(`[GitSandbox] Checking for merge conflicts...`);
690
+ const mergeCheck = this.canMergeCleanly(repoPath, branch);
691
+ if (!mergeCheck.canMerge) {
692
+ console.log(`[GitSandbox] Merge would have conflicts: ${mergeCheck.conflictFiles?.join(', ')}`);
693
+ return {
694
+ pushed: false,
695
+ merged: false,
696
+ conflict: true,
697
+ conflictFiles: mergeCheck.conflictFiles,
698
+ };
699
+ }
700
+ try {
701
+ const mainBranch = this.getMainBranch(repoPath);
702
+ console.log(`[GitSandbox] Checking out ${mainBranch} in main repo...`);
703
+ this.exec(`git checkout ${mainBranch}`, repoPath);
704
+ console.log(`[GitSandbox] Merging ${branch} into ${mainBranch}...`);
705
+ this.exec(`git merge ${branch}`, repoPath);
706
+ console.log(`[GitSandbox] Successfully merged ${branch} into ${mainBranch}`);
707
+ return { pushed: false, merged: true, conflict: false };
708
+ }
709
+ catch (e) {
710
+ console.log(`[GitSandbox] Merge failed: ${e instanceof Error ? e.message : e}`);
711
+ return { pushed: false, merged: false, conflict: false };
712
+ }
713
+ }
714
+ }
715
+ // =========== CONFLICT RESOLUTION METHODS ===========
716
+ /**
717
+ * Check if the worktree is currently in a merge conflict state
718
+ */
719
+ isInMergeConflict(worktreePath) {
720
+ try {
721
+ // Check for MERGE_HEAD which indicates an ongoing merge
722
+ const mergeHead = join(worktreePath, '.git', 'MERGE_HEAD');
723
+ if (existsSync(mergeHead)) {
724
+ return true;
725
+ }
726
+ // For worktrees, check in the git dir reference
727
+ const gitFile = join(worktreePath, '.git');
728
+ if (existsSync(gitFile)) {
729
+ const content = readFileSync(gitFile, 'utf-8').trim();
730
+ if (content.startsWith('gitdir: ')) {
731
+ const gitDir = content.substring(8);
732
+ if (existsSync(join(gitDir, 'MERGE_HEAD'))) {
733
+ return true;
734
+ }
735
+ }
736
+ }
737
+ return false;
738
+ }
739
+ catch {
740
+ return false;
741
+ }
742
+ }
743
+ /**
744
+ * Get list of files that have unresolved conflicts
745
+ */
746
+ getConflictingFiles(worktreePath) {
747
+ try {
748
+ // --diff-filter=U shows only unmerged (conflicted) files
749
+ const output = this.exec('git diff --name-only --diff-filter=U', worktreePath);
750
+ return output.split('\n').filter(Boolean);
751
+ }
752
+ catch {
753
+ return [];
754
+ }
755
+ }
756
+ /**
757
+ * Start a merge in the worktree to get into conflict state for resolution
758
+ * This is used when we know there will be conflicts and want to let user resolve them
759
+ */
760
+ startMergeForResolution(worktreePath, repoPath) {
761
+ const mainBranch = this.getMainBranch(repoPath);
762
+ try {
763
+ // Try to merge main into the worktree's branch
764
+ // This will leave files in conflict state for resolution
765
+ this.exec(`git merge ${mainBranch}`, worktreePath);
766
+ // If merge succeeded, no conflicts
767
+ return { success: true, conflictFiles: [] };
768
+ }
769
+ catch {
770
+ // Merge failed - get the conflicting files
771
+ const conflictFiles = this.getConflictingFiles(worktreePath);
772
+ return { success: false, conflictFiles };
773
+ }
774
+ }
775
+ /**
776
+ * Get detailed information about conflicting files
777
+ * Must be called when worktree is in merge conflict state
778
+ */
779
+ getConflictDetails(worktreePath) {
780
+ const conflictFiles = this.getConflictingFiles(worktreePath);
781
+ const results = [];
782
+ // Get the branch labels from MERGE_MSG or default names
783
+ let oursLabel = 'HEAD (current)';
784
+ let theirsLabel = 'incoming';
785
+ try {
786
+ const mergeMsgPath = join(worktreePath, '.git', 'MERGE_MSG');
787
+ const mergeMsg = existsSync(mergeMsgPath) ? readFileSync(mergeMsgPath, 'utf-8') : '';
788
+ const match = mergeMsg.match(/Merge branch '([^']+)'/);
789
+ if (match) {
790
+ theirsLabel = match[1];
791
+ }
792
+ }
793
+ catch {
794
+ // Use defaults
795
+ }
796
+ for (const filePath of conflictFiles) {
797
+ try {
798
+ const fullPath = join(worktreePath, filePath);
799
+ if (!existsSync(fullPath))
800
+ continue;
801
+ // Read file content to get conflict preview
802
+ const content = readFileSync(fullPath, 'utf-8');
803
+ const lines = content.split('\n');
804
+ // Find first conflict marker and extract preview
805
+ let preview = '';
806
+ let conflictCount = 0;
807
+ let inConflict = false;
808
+ let previewLines = [];
809
+ for (let i = 0; i < lines.length && previewLines.length < 30; i++) {
810
+ const line = lines[i];
811
+ if (line.startsWith('<<<<<<<')) {
812
+ conflictCount++;
813
+ inConflict = true;
814
+ if (conflictCount === 1) {
815
+ // Start capturing preview from first conflict
816
+ previewLines.push(line);
817
+ }
818
+ }
819
+ else if (line.startsWith('>>>>>>>')) {
820
+ inConflict = false;
821
+ if (conflictCount === 1) {
822
+ previewLines.push(line);
823
+ }
824
+ }
825
+ else if (inConflict && conflictCount === 1) {
826
+ previewLines.push(line);
827
+ }
828
+ }
829
+ preview = previewLines.join('\n');
830
+ if (conflictCount > 1) {
831
+ preview += `\n\n... and ${conflictCount - 1} more conflict(s)`;
832
+ }
833
+ results.push({
834
+ filePath,
835
+ preview,
836
+ oursLabel,
837
+ theirsLabel,
838
+ conflictCount,
839
+ });
840
+ }
841
+ catch (e) {
842
+ // Skip files we can't read
843
+ console.warn(`[GitSandbox] Could not read conflict file ${filePath}: ${e}`);
844
+ }
845
+ }
846
+ return results;
847
+ }
848
+ /**
849
+ * Resolve a single conflicting file using specified strategy
850
+ */
851
+ resolveConflict(worktreePath, filePath, strategy) {
852
+ console.log(`[GitSandbox] Resolving conflict for ${filePath} using ${strategy}`);
853
+ // Use git checkout with --ours or --theirs to pick one side
854
+ this.exec(`git checkout --${strategy} -- "${filePath}"`, worktreePath);
855
+ // Stage the resolved file
856
+ this.exec(`git add "${filePath}"`, worktreePath);
857
+ console.log(`[GitSandbox] Resolved ${filePath} using ${strategy} strategy`);
858
+ }
859
+ /**
860
+ * Resolve all conflicting files using specified strategy
861
+ */
862
+ resolveAllConflicts(worktreePath, strategy) {
863
+ const conflictFiles = this.getConflictingFiles(worktreePath);
864
+ console.log(`[GitSandbox] Resolving ${conflictFiles.length} conflicts using ${strategy}`);
865
+ for (const filePath of conflictFiles) {
866
+ this.resolveConflict(worktreePath, filePath, strategy);
867
+ }
868
+ }
869
+ /**
870
+ * Abort an ongoing merge and reset to pre-merge state
871
+ */
872
+ abortMerge(worktreePath) {
873
+ console.log(`[GitSandbox] Aborting merge in ${worktreePath}`);
874
+ try {
875
+ this.exec('git merge --abort', worktreePath);
876
+ console.log('[GitSandbox] Merge aborted successfully');
877
+ }
878
+ catch (e) {
879
+ // If merge abort fails, try hard reset
880
+ console.warn(`[GitSandbox] Merge abort failed, trying reset: ${e}`);
881
+ this.exec('git reset --hard HEAD', worktreePath);
882
+ }
883
+ }
884
+ /**
885
+ * Complete a merge after all conflicts are resolved
886
+ */
887
+ completeMerge(worktreePath, commitMessage) {
888
+ // Check if there are still unresolved conflicts
889
+ const conflictFiles = this.getConflictingFiles(worktreePath);
890
+ if (conflictFiles.length > 0) {
891
+ throw new Error(`Cannot complete merge - ${conflictFiles.length} files still have conflicts: ${conflictFiles.join(', ')}`);
892
+ }
893
+ // Commit the merge
894
+ const message = commitMessage || 'Merge conflict resolved';
895
+ const escapedMessage = message.replace(/"/g, '\\"');
896
+ try {
897
+ this.exec(`git commit -m "${escapedMessage}"`, worktreePath);
898
+ console.log('[GitSandbox] Merge completed successfully');
899
+ }
900
+ catch (e) {
901
+ // Might already be committed
902
+ console.warn(`[GitSandbox] Commit failed (may already be committed): ${e}`);
903
+ }
904
+ }
905
+ }
906
+ export const gitSandbox = new GitSandbox();
907
+ //# sourceMappingURL=git-sandbox.js.map