claude-wec 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 (137) hide show
  1. package/LICENSE +675 -0
  2. package/README.md +371 -0
  3. package/dist/api-docs.html +879 -0
  4. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  5. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  6. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  7. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  8. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  10. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  11. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  12. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  13. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  14. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  16. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  17. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  18. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  19. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  20. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  21. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  22. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  23. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  24. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  25. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  26. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  27. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  28. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  29. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  30. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  31. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  32. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  33. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  34. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  35. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  36. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  37. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  38. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  46. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  47. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  48. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  49. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  50. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  51. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  52. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  53. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  54. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  55. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  56. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  57. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  58. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  59. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  60. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  61. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  62. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  63. package/dist/assets/index-cIxJ4RXb.js +1226 -0
  64. package/dist/assets/index-oyEz69sP.css +32 -0
  65. package/dist/assets/vendor-codemirror-CJLzwpLB.js +39 -0
  66. package/dist/assets/vendor-react-DcyRfQm3.js +59 -0
  67. package/dist/assets/vendor-xterm-DfaPXD3y.js +66 -0
  68. package/dist/clear-cache.html +85 -0
  69. package/dist/convert-icons.md +53 -0
  70. package/dist/favicon.png +0 -0
  71. package/dist/favicon.svg +9 -0
  72. package/dist/generate-icons.js +49 -0
  73. package/dist/icons/claude-ai-icon.svg +1 -0
  74. package/dist/icons/codex-white.svg +3 -0
  75. package/dist/icons/codex.svg +3 -0
  76. package/dist/icons/cursor-white.svg +12 -0
  77. package/dist/icons/cursor.svg +1 -0
  78. package/dist/icons/generate-icons.md +19 -0
  79. package/dist/icons/icon-128x128.png +0 -0
  80. package/dist/icons/icon-128x128.svg +12 -0
  81. package/dist/icons/icon-144x144.png +0 -0
  82. package/dist/icons/icon-144x144.svg +12 -0
  83. package/dist/icons/icon-152x152.png +0 -0
  84. package/dist/icons/icon-152x152.svg +12 -0
  85. package/dist/icons/icon-192x192.png +0 -0
  86. package/dist/icons/icon-192x192.svg +12 -0
  87. package/dist/icons/icon-384x384.png +0 -0
  88. package/dist/icons/icon-384x384.svg +12 -0
  89. package/dist/icons/icon-512x512.png +0 -0
  90. package/dist/icons/icon-512x512.svg +12 -0
  91. package/dist/icons/icon-72x72.png +0 -0
  92. package/dist/icons/icon-72x72.svg +12 -0
  93. package/dist/icons/icon-96x96.png +0 -0
  94. package/dist/icons/icon-96x96.svg +12 -0
  95. package/dist/icons/icon-template.svg +12 -0
  96. package/dist/index.html +52 -0
  97. package/dist/logo-128.png +0 -0
  98. package/dist/logo-256.png +0 -0
  99. package/dist/logo-32.png +0 -0
  100. package/dist/logo-512.png +0 -0
  101. package/dist/logo-64.png +0 -0
  102. package/dist/logo.svg +17 -0
  103. package/dist/manifest.json +61 -0
  104. package/dist/screenshots/cli-selection.png +0 -0
  105. package/dist/screenshots/desktop-main.png +0 -0
  106. package/dist/screenshots/mobile-chat.png +0 -0
  107. package/dist/screenshots/tools-modal.png +0 -0
  108. package/dist/sw.js +49 -0
  109. package/package.json +109 -0
  110. package/server/claude-sdk.js +721 -0
  111. package/server/cli.js +327 -0
  112. package/server/cursor-cli.js +267 -0
  113. package/server/database/auth.db +0 -0
  114. package/server/database/db.js +361 -0
  115. package/server/database/init.sql +52 -0
  116. package/server/index.js +1747 -0
  117. package/server/middleware/auth.js +111 -0
  118. package/server/openai-codex.js +389 -0
  119. package/server/projects.js +1604 -0
  120. package/server/routes/agent.js +1230 -0
  121. package/server/routes/auth.js +135 -0
  122. package/server/routes/cli-auth.js +263 -0
  123. package/server/routes/codex.js +345 -0
  124. package/server/routes/commands.js +521 -0
  125. package/server/routes/cursor.js +795 -0
  126. package/server/routes/git.js +1128 -0
  127. package/server/routes/mcp-utils.js +48 -0
  128. package/server/routes/mcp.js +552 -0
  129. package/server/routes/projects.js +378 -0
  130. package/server/routes/settings.js +178 -0
  131. package/server/routes/taskmaster.js +1963 -0
  132. package/server/routes/user.js +106 -0
  133. package/server/utils/commandParser.js +303 -0
  134. package/server/utils/gitConfig.js +24 -0
  135. package/server/utils/mcp-detector.js +198 -0
  136. package/server/utils/taskmaster-websocket.js +129 -0
  137. package/shared/modelConstants.js +65 -0
@@ -0,0 +1,1128 @@
1
+ import express from 'express';
2
+ import { exec } from 'child_process';
3
+ import { promisify } from 'util';
4
+ import path from 'path';
5
+ import { promises as fs } from 'fs';
6
+ import { extractProjectDirectory } from '../projects.js';
7
+ import { queryClaudeSDK } from '../claude-sdk.js';
8
+ import { spawnCursor } from '../cursor-cli.js';
9
+
10
+ const router = express.Router();
11
+ const execAsync = promisify(exec);
12
+
13
+ // Helper function to get the actual project path from the encoded project name
14
+ async function getActualProjectPath(projectName) {
15
+ try {
16
+ return await extractProjectDirectory(projectName);
17
+ } catch (error) {
18
+ console.error(`Error extracting project directory for ${projectName}:`, error);
19
+ // Fallback to the old method
20
+ return projectName.replace(/-/g, '/');
21
+ }
22
+ }
23
+
24
+ // Helper function to strip git diff headers
25
+ function stripDiffHeaders(diff) {
26
+ if (!diff) return '';
27
+
28
+ const lines = diff.split('\n');
29
+ const filteredLines = [];
30
+ let startIncluding = false;
31
+
32
+ for (const line of lines) {
33
+ // Skip all header lines including diff --git, index, file mode, and --- / +++ file paths
34
+ if (line.startsWith('diff --git') ||
35
+ line.startsWith('index ') ||
36
+ line.startsWith('new file mode') ||
37
+ line.startsWith('deleted file mode') ||
38
+ line.startsWith('---') ||
39
+ line.startsWith('+++')) {
40
+ continue;
41
+ }
42
+
43
+ // Start including lines from @@ hunk headers onwards
44
+ if (line.startsWith('@@') || startIncluding) {
45
+ startIncluding = true;
46
+ filteredLines.push(line);
47
+ }
48
+ }
49
+
50
+ return filteredLines.join('\n');
51
+ }
52
+
53
+ // Helper function to validate git repository
54
+ async function validateGitRepository(projectPath) {
55
+ try {
56
+ // Check if directory exists
57
+ await fs.access(projectPath);
58
+ } catch {
59
+ throw new Error(`Project path not found: ${projectPath}`);
60
+ }
61
+
62
+ try {
63
+ // Use --show-toplevel to get the root of the git repository
64
+ const { stdout: gitRoot } = await execAsync('git rev-parse --show-toplevel', { cwd: projectPath });
65
+ const normalizedGitRoot = path.resolve(gitRoot.trim());
66
+ const normalizedProjectPath = path.resolve(projectPath);
67
+
68
+ // Ensure the git root matches our project path (prevent using parent git repos)
69
+ if (normalizedGitRoot !== normalizedProjectPath) {
70
+ throw new Error(`Project directory is not a git repository. This directory is inside a git repository at ${normalizedGitRoot}, but git operations should be run from the repository root.`);
71
+ }
72
+ } catch (error) {
73
+ if (error.message.includes('Project directory is not a git repository')) {
74
+ throw error;
75
+ }
76
+ throw new Error('Not a git repository. This directory does not contain a .git folder. Initialize a git repository with "git init" to use source control features.');
77
+ }
78
+ }
79
+
80
+ // Get git status for a project
81
+ router.get('/status', async (req, res) => {
82
+ const { project } = req.query;
83
+
84
+ if (!project) {
85
+ return res.status(400).json({ error: 'Project name is required' });
86
+ }
87
+
88
+ try {
89
+ const projectPath = await getActualProjectPath(project);
90
+
91
+ // Validate git repository
92
+ await validateGitRepository(projectPath);
93
+
94
+ // Get current branch - handle case where there are no commits yet
95
+ let branch = 'main';
96
+ let hasCommits = true;
97
+ try {
98
+ const { stdout: branchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
99
+ branch = branchOutput.trim();
100
+ } catch (error) {
101
+ // No HEAD exists - repository has no commits yet
102
+ if (error.message.includes('unknown revision') || error.message.includes('ambiguous argument')) {
103
+ hasCommits = false;
104
+ branch = 'main';
105
+ } else {
106
+ throw error;
107
+ }
108
+ }
109
+
110
+ // Get git status
111
+ const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd: projectPath });
112
+
113
+ const modified = [];
114
+ const added = [];
115
+ const deleted = [];
116
+ const untracked = [];
117
+
118
+ statusOutput.split('\n').forEach(line => {
119
+ if (!line.trim()) return;
120
+
121
+ const status = line.substring(0, 2);
122
+ const file = line.substring(3);
123
+
124
+ if (status === 'M ' || status === ' M' || status === 'MM') {
125
+ modified.push(file);
126
+ } else if (status === 'A ' || status === 'AM') {
127
+ added.push(file);
128
+ } else if (status === 'D ' || status === ' D') {
129
+ deleted.push(file);
130
+ } else if (status === '??') {
131
+ untracked.push(file);
132
+ }
133
+ });
134
+
135
+ res.json({
136
+ branch,
137
+ hasCommits,
138
+ modified,
139
+ added,
140
+ deleted,
141
+ untracked
142
+ });
143
+ } catch (error) {
144
+ console.error('Git status error:', error);
145
+ res.json({
146
+ error: error.message.includes('not a git repository') || error.message.includes('Project directory is not a git repository')
147
+ ? error.message
148
+ : 'Git operation failed',
149
+ details: error.message.includes('not a git repository') || error.message.includes('Project directory is not a git repository')
150
+ ? error.message
151
+ : `Failed to get git status: ${error.message}`
152
+ });
153
+ }
154
+ });
155
+
156
+ // Get diff for a specific file
157
+ router.get('/diff', async (req, res) => {
158
+ const { project, file } = req.query;
159
+
160
+ if (!project || !file) {
161
+ return res.status(400).json({ error: 'Project name and file path are required' });
162
+ }
163
+
164
+ try {
165
+ const projectPath = await getActualProjectPath(project);
166
+
167
+ // Validate git repository
168
+ await validateGitRepository(projectPath);
169
+
170
+ // Check if file is untracked or deleted
171
+ const { stdout: statusOutput } = await execAsync(`git status --porcelain "${file}"`, { cwd: projectPath });
172
+ const isUntracked = statusOutput.startsWith('??');
173
+ const isDeleted = statusOutput.trim().startsWith('D ') || statusOutput.trim().startsWith(' D');
174
+
175
+ let diff;
176
+ if (isUntracked) {
177
+ // For untracked files, show the entire file content as additions
178
+ const filePath = path.join(projectPath, file);
179
+ const stats = await fs.stat(filePath);
180
+
181
+ if (stats.isDirectory()) {
182
+ // For directories, show a simple message
183
+ diff = `Directory: ${file}\n(Cannot show diff for directories)`;
184
+ } else {
185
+ const fileContent = await fs.readFile(filePath, 'utf-8');
186
+ const lines = fileContent.split('\n');
187
+ diff = `--- /dev/null\n+++ b/${file}\n@@ -0,0 +1,${lines.length} @@\n` +
188
+ lines.map(line => `+${line}`).join('\n');
189
+ }
190
+ } else if (isDeleted) {
191
+ // For deleted files, show the entire file content from HEAD as deletions
192
+ const { stdout: fileContent } = await execAsync(`git show HEAD:"${file}"`, { cwd: projectPath });
193
+ const lines = fileContent.split('\n');
194
+ diff = `--- a/${file}\n+++ /dev/null\n@@ -1,${lines.length} +0,0 @@\n` +
195
+ lines.map(line => `-${line}`).join('\n');
196
+ } else {
197
+ // Get diff for tracked files
198
+ // First check for unstaged changes (working tree vs index)
199
+ const { stdout: unstagedDiff } = await execAsync(`git diff -- "${file}"`, { cwd: projectPath });
200
+
201
+ if (unstagedDiff) {
202
+ // Show unstaged changes if they exist
203
+ diff = stripDiffHeaders(unstagedDiff);
204
+ } else {
205
+ // If no unstaged changes, check for staged changes (index vs HEAD)
206
+ const { stdout: stagedDiff } = await execAsync(`git diff --cached -- "${file}"`, { cwd: projectPath });
207
+ diff = stripDiffHeaders(stagedDiff) || '';
208
+ }
209
+ }
210
+
211
+ res.json({ diff });
212
+ } catch (error) {
213
+ console.error('Git diff error:', error);
214
+ res.json({ error: error.message });
215
+ }
216
+ });
217
+
218
+ // Get file content with diff information for CodeEditor
219
+ router.get('/file-with-diff', async (req, res) => {
220
+ const { project, file } = req.query;
221
+
222
+ if (!project || !file) {
223
+ return res.status(400).json({ error: 'Project name and file path are required' });
224
+ }
225
+
226
+ try {
227
+ const projectPath = await getActualProjectPath(project);
228
+
229
+ // Validate git repository
230
+ await validateGitRepository(projectPath);
231
+
232
+ // Check file status
233
+ const { stdout: statusOutput } = await execAsync(`git status --porcelain "${file}"`, { cwd: projectPath });
234
+ const isUntracked = statusOutput.startsWith('??');
235
+ const isDeleted = statusOutput.trim().startsWith('D ') || statusOutput.trim().startsWith(' D');
236
+
237
+ let currentContent = '';
238
+ let oldContent = '';
239
+
240
+ if (isDeleted) {
241
+ // For deleted files, get content from HEAD
242
+ const { stdout: headContent } = await execAsync(`git show HEAD:"${file}"`, { cwd: projectPath });
243
+ oldContent = headContent;
244
+ currentContent = headContent; // Show the deleted content in editor
245
+ } else {
246
+ // Get current file content
247
+ const filePath = path.join(projectPath, file);
248
+ const stats = await fs.stat(filePath);
249
+
250
+ if (stats.isDirectory()) {
251
+ // Cannot show content for directories
252
+ return res.status(400).json({ error: 'Cannot show diff for directories' });
253
+ }
254
+
255
+ currentContent = await fs.readFile(filePath, 'utf-8');
256
+
257
+ if (!isUntracked) {
258
+ // Get the old content from HEAD for tracked files
259
+ try {
260
+ const { stdout: headContent } = await execAsync(`git show HEAD:"${file}"`, { cwd: projectPath });
261
+ oldContent = headContent;
262
+ } catch (error) {
263
+ // File might be newly added to git (staged but not committed)
264
+ oldContent = '';
265
+ }
266
+ }
267
+ }
268
+
269
+ res.json({
270
+ currentContent,
271
+ oldContent,
272
+ isDeleted,
273
+ isUntracked
274
+ });
275
+ } catch (error) {
276
+ console.error('Git file-with-diff error:', error);
277
+ res.json({ error: error.message });
278
+ }
279
+ });
280
+
281
+ // Create initial commit
282
+ router.post('/initial-commit', async (req, res) => {
283
+ const { project } = req.body;
284
+
285
+ if (!project) {
286
+ return res.status(400).json({ error: 'Project name is required' });
287
+ }
288
+
289
+ try {
290
+ const projectPath = await getActualProjectPath(project);
291
+
292
+ // Validate git repository
293
+ await validateGitRepository(projectPath);
294
+
295
+ // Check if there are already commits
296
+ try {
297
+ await execAsync('git rev-parse HEAD', { cwd: projectPath });
298
+ return res.status(400).json({ error: 'Repository already has commits. Use regular commit instead.' });
299
+ } catch (error) {
300
+ // No HEAD - this is good, we can create initial commit
301
+ }
302
+
303
+ // Add all files
304
+ await execAsync('git add .', { cwd: projectPath });
305
+
306
+ // Create initial commit
307
+ const { stdout } = await execAsync('git commit -m "Initial commit"', { cwd: projectPath });
308
+
309
+ res.json({ success: true, output: stdout, message: 'Initial commit created successfully' });
310
+ } catch (error) {
311
+ console.error('Git initial commit error:', error);
312
+
313
+ // Handle the case where there's nothing to commit
314
+ if (error.message.includes('nothing to commit')) {
315
+ return res.status(400).json({
316
+ error: 'Nothing to commit',
317
+ details: 'No files found in the repository. Add some files first.'
318
+ });
319
+ }
320
+
321
+ res.status(500).json({ error: error.message });
322
+ }
323
+ });
324
+
325
+ // Commit changes
326
+ router.post('/commit', async (req, res) => {
327
+ const { project, message, files } = req.body;
328
+
329
+ if (!project || !message || !files || files.length === 0) {
330
+ return res.status(400).json({ error: 'Project name, commit message, and files are required' });
331
+ }
332
+
333
+ try {
334
+ const projectPath = await getActualProjectPath(project);
335
+
336
+ // Validate git repository
337
+ await validateGitRepository(projectPath);
338
+
339
+ // Stage selected files
340
+ for (const file of files) {
341
+ await execAsync(`git add "${file}"`, { cwd: projectPath });
342
+ }
343
+
344
+ // Commit with message
345
+ const { stdout } = await execAsync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { cwd: projectPath });
346
+
347
+ res.json({ success: true, output: stdout });
348
+ } catch (error) {
349
+ console.error('Git commit error:', error);
350
+ res.status(500).json({ error: error.message });
351
+ }
352
+ });
353
+
354
+ // Get list of branches
355
+ router.get('/branches', async (req, res) => {
356
+ const { project } = req.query;
357
+
358
+ if (!project) {
359
+ return res.status(400).json({ error: 'Project name is required' });
360
+ }
361
+
362
+ try {
363
+ const projectPath = await getActualProjectPath(project);
364
+
365
+ // Validate git repository
366
+ await validateGitRepository(projectPath);
367
+
368
+ // Get all branches
369
+ const { stdout } = await execAsync('git branch -a', { cwd: projectPath });
370
+
371
+ // Parse branches
372
+ const branches = stdout
373
+ .split('\n')
374
+ .map(branch => branch.trim())
375
+ .filter(branch => branch && !branch.includes('->')) // Remove empty lines and HEAD pointer
376
+ .map(branch => {
377
+ // Remove asterisk from current branch
378
+ if (branch.startsWith('* ')) {
379
+ return branch.substring(2);
380
+ }
381
+ // Remove remotes/ prefix
382
+ if (branch.startsWith('remotes/origin/')) {
383
+ return branch.substring(15);
384
+ }
385
+ return branch;
386
+ })
387
+ .filter((branch, index, self) => self.indexOf(branch) === index); // Remove duplicates
388
+
389
+ res.json({ branches });
390
+ } catch (error) {
391
+ console.error('Git branches error:', error);
392
+ res.json({ error: error.message });
393
+ }
394
+ });
395
+
396
+ // Checkout branch
397
+ router.post('/checkout', async (req, res) => {
398
+ const { project, branch } = req.body;
399
+
400
+ if (!project || !branch) {
401
+ return res.status(400).json({ error: 'Project name and branch are required' });
402
+ }
403
+
404
+ try {
405
+ const projectPath = await getActualProjectPath(project);
406
+
407
+ // Checkout the branch
408
+ const { stdout } = await execAsync(`git checkout "${branch}"`, { cwd: projectPath });
409
+
410
+ res.json({ success: true, output: stdout });
411
+ } catch (error) {
412
+ console.error('Git checkout error:', error);
413
+ res.status(500).json({ error: error.message });
414
+ }
415
+ });
416
+
417
+ // Create new branch
418
+ router.post('/create-branch', async (req, res) => {
419
+ const { project, branch } = req.body;
420
+
421
+ if (!project || !branch) {
422
+ return res.status(400).json({ error: 'Project name and branch name are required' });
423
+ }
424
+
425
+ try {
426
+ const projectPath = await getActualProjectPath(project);
427
+
428
+ // Create and checkout new branch
429
+ const { stdout } = await execAsync(`git checkout -b "${branch}"`, { cwd: projectPath });
430
+
431
+ res.json({ success: true, output: stdout });
432
+ } catch (error) {
433
+ console.error('Git create branch error:', error);
434
+ res.status(500).json({ error: error.message });
435
+ }
436
+ });
437
+
438
+ // Get recent commits
439
+ router.get('/commits', async (req, res) => {
440
+ const { project, limit = 10 } = req.query;
441
+
442
+ if (!project) {
443
+ return res.status(400).json({ error: 'Project name is required' });
444
+ }
445
+
446
+ try {
447
+ const projectPath = await getActualProjectPath(project);
448
+
449
+ // Get commit log with stats
450
+ const { stdout } = await execAsync(
451
+ `git log --pretty=format:'%H|%an|%ae|%ad|%s' --date=relative -n ${limit}`,
452
+ { cwd: projectPath }
453
+ );
454
+
455
+ const commits = stdout
456
+ .split('\n')
457
+ .filter(line => line.trim())
458
+ .map(line => {
459
+ const [hash, author, email, date, ...messageParts] = line.split('|');
460
+ return {
461
+ hash,
462
+ author,
463
+ email,
464
+ date,
465
+ message: messageParts.join('|')
466
+ };
467
+ });
468
+
469
+ // Get stats for each commit
470
+ for (const commit of commits) {
471
+ try {
472
+ const { stdout: stats } = await execAsync(
473
+ `git show --stat --format='' ${commit.hash}`,
474
+ { cwd: projectPath }
475
+ );
476
+ commit.stats = stats.trim().split('\n').pop(); // Get the summary line
477
+ } catch (error) {
478
+ commit.stats = '';
479
+ }
480
+ }
481
+
482
+ res.json({ commits });
483
+ } catch (error) {
484
+ console.error('Git commits error:', error);
485
+ res.json({ error: error.message });
486
+ }
487
+ });
488
+
489
+ // Get diff for a specific commit
490
+ router.get('/commit-diff', async (req, res) => {
491
+ const { project, commit } = req.query;
492
+
493
+ if (!project || !commit) {
494
+ return res.status(400).json({ error: 'Project name and commit hash are required' });
495
+ }
496
+
497
+ try {
498
+ const projectPath = await getActualProjectPath(project);
499
+
500
+ // Get diff for the commit
501
+ const { stdout } = await execAsync(
502
+ `git show ${commit}`,
503
+ { cwd: projectPath }
504
+ );
505
+
506
+ res.json({ diff: stdout });
507
+ } catch (error) {
508
+ console.error('Git commit diff error:', error);
509
+ res.json({ error: error.message });
510
+ }
511
+ });
512
+
513
+ // Generate commit message based on staged changes using AI
514
+ router.post('/generate-commit-message', async (req, res) => {
515
+ const { project, files, provider = 'claude' } = req.body;
516
+
517
+ if (!project || !files || files.length === 0) {
518
+ return res.status(400).json({ error: 'Project name and files are required' });
519
+ }
520
+
521
+ // Validate provider
522
+ if (!['claude', 'cursor'].includes(provider)) {
523
+ return res.status(400).json({ error: 'provider must be "claude" or "cursor"' });
524
+ }
525
+
526
+ try {
527
+ const projectPath = await getActualProjectPath(project);
528
+
529
+ // Get diff for selected files
530
+ let diffContext = '';
531
+ for (const file of files) {
532
+ try {
533
+ const { stdout } = await execAsync(
534
+ `git diff HEAD -- "${file}"`,
535
+ { cwd: projectPath }
536
+ );
537
+ if (stdout) {
538
+ diffContext += `\n--- ${file} ---\n${stdout}`;
539
+ }
540
+ } catch (error) {
541
+ console.error(`Error getting diff for ${file}:`, error);
542
+ }
543
+ }
544
+
545
+ // If no diff found, might be untracked files
546
+ if (!diffContext.trim()) {
547
+ // Try to get content of untracked files
548
+ for (const file of files) {
549
+ try {
550
+ const filePath = path.join(projectPath, file);
551
+ const stats = await fs.stat(filePath);
552
+
553
+ if (!stats.isDirectory()) {
554
+ const content = await fs.readFile(filePath, 'utf-8');
555
+ diffContext += `\n--- ${file} (new file) ---\n${content.substring(0, 1000)}\n`;
556
+ } else {
557
+ diffContext += `\n--- ${file} (new directory) ---\n`;
558
+ }
559
+ } catch (error) {
560
+ console.error(`Error reading file ${file}:`, error);
561
+ }
562
+ }
563
+ }
564
+
565
+ // Generate commit message using AI
566
+ const message = await generateCommitMessageWithAI(files, diffContext, provider, projectPath);
567
+
568
+ res.json({ message });
569
+ } catch (error) {
570
+ console.error('Generate commit message error:', error);
571
+ res.status(500).json({ error: error.message });
572
+ }
573
+ });
574
+
575
+ /**
576
+ * Generates a commit message using AI (Claude SDK or Cursor CLI)
577
+ * @param {Array<string>} files - List of changed files
578
+ * @param {string} diffContext - Git diff content
579
+ * @param {string} provider - 'claude' or 'cursor'
580
+ * @param {string} projectPath - Project directory path
581
+ * @returns {Promise<string>} Generated commit message
582
+ */
583
+ async function generateCommitMessageWithAI(files, diffContext, provider, projectPath) {
584
+ // Create the prompt
585
+ const prompt = `Generate a conventional commit message for these changes.
586
+
587
+ REQUIREMENTS:
588
+ - Format: type(scope): subject
589
+ - Include body explaining what changed and why
590
+ - Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
591
+ - Subject under 50 chars, body wrapped at 72 chars
592
+ - Focus on user-facing changes, not implementation details
593
+ - Consider what's being added AND removed
594
+ - Return ONLY the commit message (no markdown, explanations, or code blocks)
595
+
596
+ FILES CHANGED:
597
+ ${files.map(f => `- ${f}`).join('\n')}
598
+
599
+ DIFFS:
600
+ ${diffContext.substring(0, 4000)}
601
+
602
+ Generate the commit message:`;
603
+
604
+ try {
605
+ // Create a simple writer that collects the response
606
+ let responseText = '';
607
+ const writer = {
608
+ send: (data) => {
609
+ try {
610
+ const parsed = typeof data === 'string' ? JSON.parse(data) : data;
611
+ console.log('🔍 Writer received message type:', parsed.type);
612
+
613
+ // Handle different message formats from Claude SDK and Cursor CLI
614
+ // Claude SDK sends: {type: 'claude-response', data: {message: {content: [...]}}}
615
+ if (parsed.type === 'claude-response' && parsed.data) {
616
+ const message = parsed.data.message || parsed.data;
617
+ console.log('📦 Claude response message:', JSON.stringify(message, null, 2).substring(0, 500));
618
+ if (message.content && Array.isArray(message.content)) {
619
+ // Extract text from content array
620
+ for (const item of message.content) {
621
+ if (item.type === 'text' && item.text) {
622
+ console.log('✅ Extracted text chunk:', item.text.substring(0, 100));
623
+ responseText += item.text;
624
+ }
625
+ }
626
+ }
627
+ }
628
+ // Cursor CLI sends: {type: 'cursor-output', output: '...'}
629
+ else if (parsed.type === 'cursor-output' && parsed.output) {
630
+ console.log('✅ Cursor output:', parsed.output.substring(0, 100));
631
+ responseText += parsed.output;
632
+ }
633
+ // Also handle direct text messages
634
+ else if (parsed.type === 'text' && parsed.text) {
635
+ console.log('✅ Direct text:', parsed.text.substring(0, 100));
636
+ responseText += parsed.text;
637
+ }
638
+ } catch (e) {
639
+ // Ignore parse errors
640
+ console.error('Error parsing writer data:', e);
641
+ }
642
+ },
643
+ setSessionId: () => {}, // No-op for this use case
644
+ };
645
+
646
+ console.log('🚀 Calling AI agent with provider:', provider);
647
+ console.log('📝 Prompt length:', prompt.length);
648
+
649
+ // Call the appropriate agent
650
+ if (provider === 'claude') {
651
+ await queryClaudeSDK(prompt, {
652
+ cwd: projectPath,
653
+ permissionMode: 'bypassPermissions',
654
+ model: 'sonnet'
655
+ }, writer);
656
+ } else if (provider === 'cursor') {
657
+ await spawnCursor(prompt, {
658
+ cwd: projectPath,
659
+ skipPermissions: true
660
+ }, writer);
661
+ }
662
+
663
+ console.log('📊 Total response text collected:', responseText.length, 'characters');
664
+ console.log('📄 Response preview:', responseText.substring(0, 200));
665
+
666
+ // Clean up the response
667
+ const cleanedMessage = cleanCommitMessage(responseText);
668
+ console.log('🧹 Cleaned message:', cleanedMessage.substring(0, 200));
669
+
670
+ return cleanedMessage || 'chore: update files';
671
+ } catch (error) {
672
+ console.error('Error generating commit message with AI:', error);
673
+ // Fallback to simple message
674
+ return `chore: update ${files.length} file${files.length !== 1 ? 's' : ''}`;
675
+ }
676
+ }
677
+
678
+ /**
679
+ * Cleans the AI-generated commit message by removing markdown, code blocks, and extra formatting
680
+ * @param {string} text - Raw AI response
681
+ * @returns {string} Clean commit message
682
+ */
683
+ function cleanCommitMessage(text) {
684
+ if (!text || !text.trim()) {
685
+ return '';
686
+ }
687
+
688
+ let cleaned = text.trim();
689
+
690
+ // Remove markdown code blocks
691
+ cleaned = cleaned.replace(/```[a-z]*\n/g, '');
692
+ cleaned = cleaned.replace(/```/g, '');
693
+
694
+ // Remove markdown headers
695
+ cleaned = cleaned.replace(/^#+\s*/gm, '');
696
+
697
+ // Remove leading/trailing quotes
698
+ cleaned = cleaned.replace(/^["']|["']$/g, '');
699
+
700
+ // If there are multiple lines, take everything (subject + body)
701
+ // Just clean up extra blank lines
702
+ cleaned = cleaned.replace(/\n{3,}/g, '\n\n');
703
+
704
+ // Remove any explanatory text before the actual commit message
705
+ // Look for conventional commit pattern and start from there
706
+ const conventionalCommitMatch = cleaned.match(/(feat|fix|docs|style|refactor|perf|test|build|ci|chore)(\(.+?\))?:.+/s);
707
+ if (conventionalCommitMatch) {
708
+ cleaned = cleaned.substring(cleaned.indexOf(conventionalCommitMatch[0]));
709
+ }
710
+
711
+ return cleaned.trim();
712
+ }
713
+
714
+ // Get remote status (ahead/behind commits with smart remote detection)
715
+ router.get('/remote-status', async (req, res) => {
716
+ const { project } = req.query;
717
+
718
+ if (!project) {
719
+ return res.status(400).json({ error: 'Project name is required' });
720
+ }
721
+
722
+ try {
723
+ const projectPath = await getActualProjectPath(project);
724
+ await validateGitRepository(projectPath);
725
+
726
+ // Get current branch
727
+ const { stdout: currentBranch } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
728
+ const branch = currentBranch.trim();
729
+
730
+ // Check if there's a remote tracking branch (smart detection)
731
+ let trackingBranch;
732
+ let remoteName;
733
+ try {
734
+ const { stdout } = await execAsync(`git rev-parse --abbrev-ref ${branch}@{upstream}`, { cwd: projectPath });
735
+ trackingBranch = stdout.trim();
736
+ remoteName = trackingBranch.split('/')[0]; // Extract remote name (e.g., "origin/main" -> "origin")
737
+ } catch (error) {
738
+ // No upstream branch configured - but check if we have remotes
739
+ let hasRemote = false;
740
+ let remoteName = null;
741
+ try {
742
+ const { stdout } = await execAsync('git remote', { cwd: projectPath });
743
+ const remotes = stdout.trim().split('\n').filter(r => r.trim());
744
+ if (remotes.length > 0) {
745
+ hasRemote = true;
746
+ remoteName = remotes.includes('origin') ? 'origin' : remotes[0];
747
+ }
748
+ } catch (remoteError) {
749
+ // No remotes configured
750
+ }
751
+
752
+ return res.json({
753
+ hasRemote,
754
+ hasUpstream: false,
755
+ branch,
756
+ remoteName,
757
+ message: 'No remote tracking branch configured'
758
+ });
759
+ }
760
+
761
+ // Get ahead/behind counts
762
+ const { stdout: countOutput } = await execAsync(
763
+ `git rev-list --count --left-right ${trackingBranch}...HEAD`,
764
+ { cwd: projectPath }
765
+ );
766
+
767
+ const [behind, ahead] = countOutput.trim().split('\t').map(Number);
768
+
769
+ res.json({
770
+ hasRemote: true,
771
+ hasUpstream: true,
772
+ branch,
773
+ remoteBranch: trackingBranch,
774
+ remoteName,
775
+ ahead: ahead || 0,
776
+ behind: behind || 0,
777
+ isUpToDate: ahead === 0 && behind === 0
778
+ });
779
+ } catch (error) {
780
+ console.error('Git remote status error:', error);
781
+ res.json({ error: error.message });
782
+ }
783
+ });
784
+
785
+ // Fetch from remote (using smart remote detection)
786
+ router.post('/fetch', async (req, res) => {
787
+ const { project } = req.body;
788
+
789
+ if (!project) {
790
+ return res.status(400).json({ error: 'Project name is required' });
791
+ }
792
+
793
+ try {
794
+ const projectPath = await getActualProjectPath(project);
795
+ await validateGitRepository(projectPath);
796
+
797
+ // Get current branch and its upstream remote
798
+ const { stdout: currentBranch } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
799
+ const branch = currentBranch.trim();
800
+
801
+ let remoteName = 'origin'; // fallback
802
+ try {
803
+ const { stdout } = await execAsync(`git rev-parse --abbrev-ref ${branch}@{upstream}`, { cwd: projectPath });
804
+ remoteName = stdout.trim().split('/')[0]; // Extract remote name
805
+ } catch (error) {
806
+ // No upstream, try to fetch from origin anyway
807
+ console.log('No upstream configured, using origin as fallback');
808
+ }
809
+
810
+ const { stdout } = await execAsync(`git fetch ${remoteName}`, { cwd: projectPath });
811
+
812
+ res.json({ success: true, output: stdout || 'Fetch completed successfully', remoteName });
813
+ } catch (error) {
814
+ console.error('Git fetch error:', error);
815
+ res.status(500).json({
816
+ error: 'Fetch failed',
817
+ details: error.message.includes('Could not resolve hostname')
818
+ ? 'Unable to connect to remote repository. Check your internet connection.'
819
+ : error.message.includes('fatal: \'origin\' does not appear to be a git repository')
820
+ ? 'No remote repository configured. Add a remote with: git remote add origin <url>'
821
+ : error.message
822
+ });
823
+ }
824
+ });
825
+
826
+ // Pull from remote (fetch + merge using smart remote detection)
827
+ router.post('/pull', async (req, res) => {
828
+ const { project } = req.body;
829
+
830
+ if (!project) {
831
+ return res.status(400).json({ error: 'Project name is required' });
832
+ }
833
+
834
+ try {
835
+ const projectPath = await getActualProjectPath(project);
836
+ await validateGitRepository(projectPath);
837
+
838
+ // Get current branch and its upstream remote
839
+ const { stdout: currentBranch } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
840
+ const branch = currentBranch.trim();
841
+
842
+ let remoteName = 'origin'; // fallback
843
+ let remoteBranch = branch; // fallback
844
+ try {
845
+ const { stdout } = await execAsync(`git rev-parse --abbrev-ref ${branch}@{upstream}`, { cwd: projectPath });
846
+ const tracking = stdout.trim();
847
+ remoteName = tracking.split('/')[0]; // Extract remote name
848
+ remoteBranch = tracking.split('/').slice(1).join('/'); // Extract branch name
849
+ } catch (error) {
850
+ // No upstream, use fallback
851
+ console.log('No upstream configured, using origin/branch as fallback');
852
+ }
853
+
854
+ const { stdout } = await execAsync(`git pull ${remoteName} ${remoteBranch}`, { cwd: projectPath });
855
+
856
+ res.json({
857
+ success: true,
858
+ output: stdout || 'Pull completed successfully',
859
+ remoteName,
860
+ remoteBranch
861
+ });
862
+ } catch (error) {
863
+ console.error('Git pull error:', error);
864
+
865
+ // Enhanced error handling for common pull scenarios
866
+ let errorMessage = 'Pull failed';
867
+ let details = error.message;
868
+
869
+ if (error.message.includes('CONFLICT')) {
870
+ errorMessage = 'Merge conflicts detected';
871
+ details = 'Pull created merge conflicts. Please resolve conflicts manually in the editor, then commit the changes.';
872
+ } else if (error.message.includes('Please commit your changes or stash them')) {
873
+ errorMessage = 'Uncommitted changes detected';
874
+ details = 'Please commit or stash your local changes before pulling.';
875
+ } else if (error.message.includes('Could not resolve hostname')) {
876
+ errorMessage = 'Network error';
877
+ details = 'Unable to connect to remote repository. Check your internet connection.';
878
+ } else if (error.message.includes('fatal: \'origin\' does not appear to be a git repository')) {
879
+ errorMessage = 'Remote not configured';
880
+ details = 'No remote repository configured. Add a remote with: git remote add origin <url>';
881
+ } else if (error.message.includes('diverged')) {
882
+ errorMessage = 'Branches have diverged';
883
+ details = 'Your local branch and remote branch have diverged. Consider fetching first to review changes.';
884
+ }
885
+
886
+ res.status(500).json({
887
+ error: errorMessage,
888
+ details: details
889
+ });
890
+ }
891
+ });
892
+
893
+ // Push commits to remote repository
894
+ router.post('/push', async (req, res) => {
895
+ const { project } = req.body;
896
+
897
+ if (!project) {
898
+ return res.status(400).json({ error: 'Project name is required' });
899
+ }
900
+
901
+ try {
902
+ const projectPath = await getActualProjectPath(project);
903
+ await validateGitRepository(projectPath);
904
+
905
+ // Get current branch and its upstream remote
906
+ const { stdout: currentBranch } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
907
+ const branch = currentBranch.trim();
908
+
909
+ let remoteName = 'origin'; // fallback
910
+ let remoteBranch = branch; // fallback
911
+ try {
912
+ const { stdout } = await execAsync(`git rev-parse --abbrev-ref ${branch}@{upstream}`, { cwd: projectPath });
913
+ const tracking = stdout.trim();
914
+ remoteName = tracking.split('/')[0]; // Extract remote name
915
+ remoteBranch = tracking.split('/').slice(1).join('/'); // Extract branch name
916
+ } catch (error) {
917
+ // No upstream, use fallback
918
+ console.log('No upstream configured, using origin/branch as fallback');
919
+ }
920
+
921
+ const { stdout } = await execAsync(`git push ${remoteName} ${remoteBranch}`, { cwd: projectPath });
922
+
923
+ res.json({
924
+ success: true,
925
+ output: stdout || 'Push completed successfully',
926
+ remoteName,
927
+ remoteBranch
928
+ });
929
+ } catch (error) {
930
+ console.error('Git push error:', error);
931
+
932
+ // Enhanced error handling for common push scenarios
933
+ let errorMessage = 'Push failed';
934
+ let details = error.message;
935
+
936
+ if (error.message.includes('rejected')) {
937
+ errorMessage = 'Push rejected';
938
+ details = 'The remote has newer commits. Pull first to merge changes before pushing.';
939
+ } else if (error.message.includes('non-fast-forward')) {
940
+ errorMessage = 'Non-fast-forward push';
941
+ details = 'Your branch is behind the remote. Pull the latest changes first.';
942
+ } else if (error.message.includes('Could not resolve hostname')) {
943
+ errorMessage = 'Network error';
944
+ details = 'Unable to connect to remote repository. Check your internet connection.';
945
+ } else if (error.message.includes('fatal: \'origin\' does not appear to be a git repository')) {
946
+ errorMessage = 'Remote not configured';
947
+ details = 'No remote repository configured. Add a remote with: git remote add origin <url>';
948
+ } else if (error.message.includes('Permission denied')) {
949
+ errorMessage = 'Authentication failed';
950
+ details = 'Permission denied. Check your credentials or SSH keys.';
951
+ } else if (error.message.includes('no upstream branch')) {
952
+ errorMessage = 'No upstream branch';
953
+ details = 'No upstream branch configured. Use: git push --set-upstream origin <branch>';
954
+ }
955
+
956
+ res.status(500).json({
957
+ error: errorMessage,
958
+ details: details
959
+ });
960
+ }
961
+ });
962
+
963
+ // Publish branch to remote (set upstream and push)
964
+ router.post('/publish', async (req, res) => {
965
+ const { project, branch } = req.body;
966
+
967
+ if (!project || !branch) {
968
+ return res.status(400).json({ error: 'Project name and branch are required' });
969
+ }
970
+
971
+ try {
972
+ const projectPath = await getActualProjectPath(project);
973
+ await validateGitRepository(projectPath);
974
+
975
+ // Get current branch to verify it matches the requested branch
976
+ const { stdout: currentBranch } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
977
+ const currentBranchName = currentBranch.trim();
978
+
979
+ if (currentBranchName !== branch) {
980
+ return res.status(400).json({
981
+ error: `Branch mismatch. Current branch is ${currentBranchName}, but trying to publish ${branch}`
982
+ });
983
+ }
984
+
985
+ // Check if remote exists
986
+ let remoteName = 'origin';
987
+ try {
988
+ const { stdout } = await execAsync('git remote', { cwd: projectPath });
989
+ const remotes = stdout.trim().split('\n').filter(r => r.trim());
990
+ if (remotes.length === 0) {
991
+ return res.status(400).json({
992
+ error: 'No remote repository configured. Add a remote with: git remote add origin <url>'
993
+ });
994
+ }
995
+ remoteName = remotes.includes('origin') ? 'origin' : remotes[0];
996
+ } catch (error) {
997
+ return res.status(400).json({
998
+ error: 'No remote repository configured. Add a remote with: git remote add origin <url>'
999
+ });
1000
+ }
1001
+
1002
+ // Publish the branch (set upstream and push)
1003
+ const { stdout } = await execAsync(`git push --set-upstream ${remoteName} ${branch}`, { cwd: projectPath });
1004
+
1005
+ res.json({
1006
+ success: true,
1007
+ output: stdout || 'Branch published successfully',
1008
+ remoteName,
1009
+ branch
1010
+ });
1011
+ } catch (error) {
1012
+ console.error('Git publish error:', error);
1013
+
1014
+ // Enhanced error handling for common publish scenarios
1015
+ let errorMessage = 'Publish failed';
1016
+ let details = error.message;
1017
+
1018
+ if (error.message.includes('rejected')) {
1019
+ errorMessage = 'Publish rejected';
1020
+ details = 'The remote branch already exists and has different commits. Use push instead.';
1021
+ } else if (error.message.includes('Could not resolve hostname')) {
1022
+ errorMessage = 'Network error';
1023
+ details = 'Unable to connect to remote repository. Check your internet connection.';
1024
+ } else if (error.message.includes('Permission denied')) {
1025
+ errorMessage = 'Authentication failed';
1026
+ details = 'Permission denied. Check your credentials or SSH keys.';
1027
+ } else if (error.message.includes('fatal:') && error.message.includes('does not appear to be a git repository')) {
1028
+ errorMessage = 'Remote not configured';
1029
+ details = 'Remote repository not properly configured. Check your remote URL.';
1030
+ }
1031
+
1032
+ res.status(500).json({
1033
+ error: errorMessage,
1034
+ details: details
1035
+ });
1036
+ }
1037
+ });
1038
+
1039
+ // Discard changes for a specific file
1040
+ router.post('/discard', async (req, res) => {
1041
+ const { project, file } = req.body;
1042
+
1043
+ if (!project || !file) {
1044
+ return res.status(400).json({ error: 'Project name and file path are required' });
1045
+ }
1046
+
1047
+ try {
1048
+ const projectPath = await getActualProjectPath(project);
1049
+ await validateGitRepository(projectPath);
1050
+
1051
+ // Check file status to determine correct discard command
1052
+ const { stdout: statusOutput } = await execAsync(`git status --porcelain "${file}"`, { cwd: projectPath });
1053
+
1054
+ if (!statusOutput.trim()) {
1055
+ return res.status(400).json({ error: 'No changes to discard for this file' });
1056
+ }
1057
+
1058
+ const status = statusOutput.substring(0, 2);
1059
+
1060
+ if (status === '??') {
1061
+ // Untracked file or directory - delete it
1062
+ const filePath = path.join(projectPath, file);
1063
+ const stats = await fs.stat(filePath);
1064
+
1065
+ if (stats.isDirectory()) {
1066
+ await fs.rm(filePath, { recursive: true, force: true });
1067
+ } else {
1068
+ await fs.unlink(filePath);
1069
+ }
1070
+ } else if (status.includes('M') || status.includes('D')) {
1071
+ // Modified or deleted file - restore from HEAD
1072
+ await execAsync(`git restore "${file}"`, { cwd: projectPath });
1073
+ } else if (status.includes('A')) {
1074
+ // Added file - unstage it
1075
+ await execAsync(`git reset HEAD "${file}"`, { cwd: projectPath });
1076
+ }
1077
+
1078
+ res.json({ success: true, message: `Changes discarded for ${file}` });
1079
+ } catch (error) {
1080
+ console.error('Git discard error:', error);
1081
+ res.status(500).json({ error: error.message });
1082
+ }
1083
+ });
1084
+
1085
+ // Delete untracked file
1086
+ router.post('/delete-untracked', async (req, res) => {
1087
+ const { project, file } = req.body;
1088
+
1089
+ if (!project || !file) {
1090
+ return res.status(400).json({ error: 'Project name and file path are required' });
1091
+ }
1092
+
1093
+ try {
1094
+ const projectPath = await getActualProjectPath(project);
1095
+ await validateGitRepository(projectPath);
1096
+
1097
+ // Check if file is actually untracked
1098
+ const { stdout: statusOutput } = await execAsync(`git status --porcelain "${file}"`, { cwd: projectPath });
1099
+
1100
+ if (!statusOutput.trim()) {
1101
+ return res.status(400).json({ error: 'File is not untracked or does not exist' });
1102
+ }
1103
+
1104
+ const status = statusOutput.substring(0, 2);
1105
+
1106
+ if (status !== '??') {
1107
+ return res.status(400).json({ error: 'File is not untracked. Use discard for tracked files.' });
1108
+ }
1109
+
1110
+ // Delete the untracked file or directory
1111
+ const filePath = path.join(projectPath, file);
1112
+ const stats = await fs.stat(filePath);
1113
+
1114
+ if (stats.isDirectory()) {
1115
+ // Use rm with recursive option for directories
1116
+ await fs.rm(filePath, { recursive: true, force: true });
1117
+ res.json({ success: true, message: `Untracked directory ${file} deleted successfully` });
1118
+ } else {
1119
+ await fs.unlink(filePath);
1120
+ res.json({ success: true, message: `Untracked file ${file} deleted successfully` });
1121
+ }
1122
+ } catch (error) {
1123
+ console.error('Git delete untracked error:', error);
1124
+ res.status(500).json({ error: error.message });
1125
+ }
1126
+ });
1127
+
1128
+ export default router;