@siteboon/claude-code-ui 1.8.2

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 (106) hide show
  1. package/.env.example +12 -0
  2. package/.nvmrc +1 -0
  3. package/LICENSE +675 -0
  4. package/README.md +275 -0
  5. package/index.html +48 -0
  6. package/package.json +84 -0
  7. package/postcss.config.js +6 -0
  8. package/public/convert-icons.md +53 -0
  9. package/public/favicon.png +0 -0
  10. package/public/favicon.svg +9 -0
  11. package/public/generate-icons.js +49 -0
  12. package/public/icons/claude-ai-icon.svg +1 -0
  13. package/public/icons/cursor.svg +1 -0
  14. package/public/icons/generate-icons.md +19 -0
  15. package/public/icons/icon-128x128.png +0 -0
  16. package/public/icons/icon-128x128.svg +12 -0
  17. package/public/icons/icon-144x144.png +0 -0
  18. package/public/icons/icon-144x144.svg +12 -0
  19. package/public/icons/icon-152x152.png +0 -0
  20. package/public/icons/icon-152x152.svg +12 -0
  21. package/public/icons/icon-192x192.png +0 -0
  22. package/public/icons/icon-192x192.svg +12 -0
  23. package/public/icons/icon-384x384.png +0 -0
  24. package/public/icons/icon-384x384.svg +12 -0
  25. package/public/icons/icon-512x512.png +0 -0
  26. package/public/icons/icon-512x512.svg +12 -0
  27. package/public/icons/icon-72x72.png +0 -0
  28. package/public/icons/icon-72x72.svg +12 -0
  29. package/public/icons/icon-96x96.png +0 -0
  30. package/public/icons/icon-96x96.svg +12 -0
  31. package/public/icons/icon-template.svg +12 -0
  32. package/public/logo.svg +9 -0
  33. package/public/manifest.json +61 -0
  34. package/public/screenshots/cli-selection.png +0 -0
  35. package/public/screenshots/desktop-main.png +0 -0
  36. package/public/screenshots/mobile-chat.png +0 -0
  37. package/public/screenshots/tools-modal.png +0 -0
  38. package/public/sw.js +49 -0
  39. package/server/claude-cli.js +391 -0
  40. package/server/cursor-cli.js +250 -0
  41. package/server/database/db.js +86 -0
  42. package/server/database/init.sql +16 -0
  43. package/server/index.js +1167 -0
  44. package/server/middleware/auth.js +80 -0
  45. package/server/projects.js +1063 -0
  46. package/server/routes/auth.js +135 -0
  47. package/server/routes/cursor.js +794 -0
  48. package/server/routes/git.js +823 -0
  49. package/server/routes/mcp-utils.js +48 -0
  50. package/server/routes/mcp.js +552 -0
  51. package/server/routes/taskmaster.js +1971 -0
  52. package/server/utils/mcp-detector.js +198 -0
  53. package/server/utils/taskmaster-websocket.js +129 -0
  54. package/src/App.jsx +751 -0
  55. package/src/components/ChatInterface.jsx +3485 -0
  56. package/src/components/ClaudeLogo.jsx +11 -0
  57. package/src/components/ClaudeStatus.jsx +107 -0
  58. package/src/components/CodeEditor.jsx +422 -0
  59. package/src/components/CreateTaskModal.jsx +88 -0
  60. package/src/components/CursorLogo.jsx +9 -0
  61. package/src/components/DarkModeToggle.jsx +35 -0
  62. package/src/components/DiffViewer.jsx +41 -0
  63. package/src/components/ErrorBoundary.jsx +73 -0
  64. package/src/components/FileTree.jsx +480 -0
  65. package/src/components/GitPanel.jsx +1283 -0
  66. package/src/components/ImageViewer.jsx +54 -0
  67. package/src/components/LoginForm.jsx +110 -0
  68. package/src/components/MainContent.jsx +577 -0
  69. package/src/components/MicButton.jsx +272 -0
  70. package/src/components/MobileNav.jsx +88 -0
  71. package/src/components/NextTaskBanner.jsx +695 -0
  72. package/src/components/PRDEditor.jsx +871 -0
  73. package/src/components/ProtectedRoute.jsx +44 -0
  74. package/src/components/QuickSettingsPanel.jsx +262 -0
  75. package/src/components/Settings.jsx +2023 -0
  76. package/src/components/SetupForm.jsx +135 -0
  77. package/src/components/Shell.jsx +663 -0
  78. package/src/components/Sidebar.jsx +1665 -0
  79. package/src/components/StandaloneShell.jsx +106 -0
  80. package/src/components/TaskCard.jsx +210 -0
  81. package/src/components/TaskDetail.jsx +406 -0
  82. package/src/components/TaskIndicator.jsx +108 -0
  83. package/src/components/TaskList.jsx +1054 -0
  84. package/src/components/TaskMasterSetupWizard.jsx +603 -0
  85. package/src/components/TaskMasterStatus.jsx +86 -0
  86. package/src/components/TodoList.jsx +91 -0
  87. package/src/components/Tooltip.jsx +91 -0
  88. package/src/components/ui/badge.jsx +31 -0
  89. package/src/components/ui/button.jsx +46 -0
  90. package/src/components/ui/input.jsx +19 -0
  91. package/src/components/ui/scroll-area.jsx +23 -0
  92. package/src/contexts/AuthContext.jsx +158 -0
  93. package/src/contexts/TaskMasterContext.jsx +324 -0
  94. package/src/contexts/TasksSettingsContext.jsx +95 -0
  95. package/src/contexts/ThemeContext.jsx +94 -0
  96. package/src/contexts/WebSocketContext.jsx +29 -0
  97. package/src/hooks/useAudioRecorder.js +109 -0
  98. package/src/hooks/useVersionCheck.js +39 -0
  99. package/src/index.css +822 -0
  100. package/src/lib/utils.js +6 -0
  101. package/src/main.jsx +10 -0
  102. package/src/utils/api.js +141 -0
  103. package/src/utils/websocket.js +109 -0
  104. package/src/utils/whisper.js +37 -0
  105. package/tailwind.config.js +63 -0
  106. package/vite.config.js +29 -0
@@ -0,0 +1,823 @@
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
+
8
+ const router = express.Router();
9
+ const execAsync = promisify(exec);
10
+
11
+ // Helper function to get the actual project path from the encoded project name
12
+ async function getActualProjectPath(projectName) {
13
+ try {
14
+ return await extractProjectDirectory(projectName);
15
+ } catch (error) {
16
+ console.error(`Error extracting project directory for ${projectName}:`, error);
17
+ // Fallback to the old method
18
+ return projectName.replace(/-/g, '/');
19
+ }
20
+ }
21
+
22
+ // Helper function to validate git repository
23
+ async function validateGitRepository(projectPath) {
24
+ try {
25
+ // Check if directory exists
26
+ await fs.access(projectPath);
27
+ } catch {
28
+ throw new Error(`Project path not found: ${projectPath}`);
29
+ }
30
+
31
+ try {
32
+ // Use --show-toplevel to get the root of the git repository
33
+ const { stdout: gitRoot } = await execAsync('git rev-parse --show-toplevel', { cwd: projectPath });
34
+ const normalizedGitRoot = path.resolve(gitRoot.trim());
35
+ const normalizedProjectPath = path.resolve(projectPath);
36
+
37
+ // Ensure the git root matches our project path (prevent using parent git repos)
38
+ if (normalizedGitRoot !== normalizedProjectPath) {
39
+ 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.`);
40
+ }
41
+ } catch (error) {
42
+ if (error.message.includes('Project directory is not a git repository')) {
43
+ throw error;
44
+ }
45
+ 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.');
46
+ }
47
+ }
48
+
49
+ // Get git status for a project
50
+ router.get('/status', async (req, res) => {
51
+ const { project } = req.query;
52
+
53
+ if (!project) {
54
+ return res.status(400).json({ error: 'Project name is required' });
55
+ }
56
+
57
+ try {
58
+ const projectPath = await getActualProjectPath(project);
59
+
60
+ // Validate git repository
61
+ await validateGitRepository(projectPath);
62
+
63
+ // Get current branch
64
+ const { stdout: branch } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
65
+
66
+ // Get git status
67
+ const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd: projectPath });
68
+
69
+ const modified = [];
70
+ const added = [];
71
+ const deleted = [];
72
+ const untracked = [];
73
+
74
+ statusOutput.split('\n').forEach(line => {
75
+ if (!line.trim()) return;
76
+
77
+ const status = line.substring(0, 2);
78
+ const file = line.substring(3);
79
+
80
+ if (status === 'M ' || status === ' M' || status === 'MM') {
81
+ modified.push(file);
82
+ } else if (status === 'A ' || status === 'AM') {
83
+ added.push(file);
84
+ } else if (status === 'D ' || status === ' D') {
85
+ deleted.push(file);
86
+ } else if (status === '??') {
87
+ untracked.push(file);
88
+ }
89
+ });
90
+
91
+ res.json({
92
+ branch: branch.trim(),
93
+ modified,
94
+ added,
95
+ deleted,
96
+ untracked
97
+ });
98
+ } catch (error) {
99
+ console.error('Git status error:', error);
100
+ res.json({
101
+ error: error.message.includes('not a git repository') || error.message.includes('Project directory is not a git repository')
102
+ ? error.message
103
+ : 'Git operation failed',
104
+ details: error.message.includes('not a git repository') || error.message.includes('Project directory is not a git repository')
105
+ ? error.message
106
+ : `Failed to get git status: ${error.message}`
107
+ });
108
+ }
109
+ });
110
+
111
+ // Get diff for a specific file
112
+ router.get('/diff', async (req, res) => {
113
+ const { project, file } = req.query;
114
+
115
+ if (!project || !file) {
116
+ return res.status(400).json({ error: 'Project name and file path are required' });
117
+ }
118
+
119
+ try {
120
+ const projectPath = await getActualProjectPath(project);
121
+
122
+ // Validate git repository
123
+ await validateGitRepository(projectPath);
124
+
125
+ // Check if file is untracked
126
+ const { stdout: statusOutput } = await execAsync(`git status --porcelain "${file}"`, { cwd: projectPath });
127
+ const isUntracked = statusOutput.startsWith('??');
128
+
129
+ let diff;
130
+ if (isUntracked) {
131
+ // For untracked files, show the entire file content as additions
132
+ const fileContent = await fs.readFile(path.join(projectPath, file), 'utf-8');
133
+ const lines = fileContent.split('\n');
134
+ diff = `--- /dev/null\n+++ b/${file}\n@@ -0,0 +1,${lines.length} @@\n` +
135
+ lines.map(line => `+${line}`).join('\n');
136
+ } else {
137
+ // Get diff for tracked files
138
+ // First check for unstaged changes (working tree vs index)
139
+ const { stdout: unstagedDiff } = await execAsync(`git diff -- "${file}"`, { cwd: projectPath });
140
+
141
+ if (unstagedDiff) {
142
+ // Show unstaged changes if they exist
143
+ diff = unstagedDiff;
144
+ } else {
145
+ // If no unstaged changes, check for staged changes (index vs HEAD)
146
+ const { stdout: stagedDiff } = await execAsync(`git diff --cached -- "${file}"`, { cwd: projectPath });
147
+ diff = stagedDiff || '';
148
+ }
149
+ }
150
+
151
+ res.json({ diff });
152
+ } catch (error) {
153
+ console.error('Git diff error:', error);
154
+ res.json({ error: error.message });
155
+ }
156
+ });
157
+
158
+ // Commit changes
159
+ router.post('/commit', async (req, res) => {
160
+ const { project, message, files } = req.body;
161
+
162
+ if (!project || !message || !files || files.length === 0) {
163
+ return res.status(400).json({ error: 'Project name, commit message, and files are required' });
164
+ }
165
+
166
+ try {
167
+ const projectPath = await getActualProjectPath(project);
168
+
169
+ // Validate git repository
170
+ await validateGitRepository(projectPath);
171
+
172
+ // Stage selected files
173
+ for (const file of files) {
174
+ await execAsync(`git add "${file}"`, { cwd: projectPath });
175
+ }
176
+
177
+ // Commit with message
178
+ const { stdout } = await execAsync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { cwd: projectPath });
179
+
180
+ res.json({ success: true, output: stdout });
181
+ } catch (error) {
182
+ console.error('Git commit error:', error);
183
+ res.status(500).json({ error: error.message });
184
+ }
185
+ });
186
+
187
+ // Get list of branches
188
+ router.get('/branches', async (req, res) => {
189
+ const { project } = req.query;
190
+
191
+ if (!project) {
192
+ return res.status(400).json({ error: 'Project name is required' });
193
+ }
194
+
195
+ try {
196
+ const projectPath = await getActualProjectPath(project);
197
+
198
+ // Validate git repository
199
+ await validateGitRepository(projectPath);
200
+
201
+ // Get all branches
202
+ const { stdout } = await execAsync('git branch -a', { cwd: projectPath });
203
+
204
+ // Parse branches
205
+ const branches = stdout
206
+ .split('\n')
207
+ .map(branch => branch.trim())
208
+ .filter(branch => branch && !branch.includes('->')) // Remove empty lines and HEAD pointer
209
+ .map(branch => {
210
+ // Remove asterisk from current branch
211
+ if (branch.startsWith('* ')) {
212
+ return branch.substring(2);
213
+ }
214
+ // Remove remotes/ prefix
215
+ if (branch.startsWith('remotes/origin/')) {
216
+ return branch.substring(15);
217
+ }
218
+ return branch;
219
+ })
220
+ .filter((branch, index, self) => self.indexOf(branch) === index); // Remove duplicates
221
+
222
+ res.json({ branches });
223
+ } catch (error) {
224
+ console.error('Git branches error:', error);
225
+ res.json({ error: error.message });
226
+ }
227
+ });
228
+
229
+ // Checkout branch
230
+ router.post('/checkout', async (req, res) => {
231
+ const { project, branch } = req.body;
232
+
233
+ if (!project || !branch) {
234
+ return res.status(400).json({ error: 'Project name and branch are required' });
235
+ }
236
+
237
+ try {
238
+ const projectPath = await getActualProjectPath(project);
239
+
240
+ // Checkout the branch
241
+ const { stdout } = await execAsync(`git checkout "${branch}"`, { cwd: projectPath });
242
+
243
+ res.json({ success: true, output: stdout });
244
+ } catch (error) {
245
+ console.error('Git checkout error:', error);
246
+ res.status(500).json({ error: error.message });
247
+ }
248
+ });
249
+
250
+ // Create new branch
251
+ router.post('/create-branch', async (req, res) => {
252
+ const { project, branch } = req.body;
253
+
254
+ if (!project || !branch) {
255
+ return res.status(400).json({ error: 'Project name and branch name are required' });
256
+ }
257
+
258
+ try {
259
+ const projectPath = await getActualProjectPath(project);
260
+
261
+ // Create and checkout new branch
262
+ const { stdout } = await execAsync(`git checkout -b "${branch}"`, { cwd: projectPath });
263
+
264
+ res.json({ success: true, output: stdout });
265
+ } catch (error) {
266
+ console.error('Git create branch error:', error);
267
+ res.status(500).json({ error: error.message });
268
+ }
269
+ });
270
+
271
+ // Get recent commits
272
+ router.get('/commits', async (req, res) => {
273
+ const { project, limit = 10 } = req.query;
274
+
275
+ if (!project) {
276
+ return res.status(400).json({ error: 'Project name is required' });
277
+ }
278
+
279
+ try {
280
+ const projectPath = await getActualProjectPath(project);
281
+
282
+ // Get commit log with stats
283
+ const { stdout } = await execAsync(
284
+ `git log --pretty=format:'%H|%an|%ae|%ad|%s' --date=relative -n ${limit}`,
285
+ { cwd: projectPath }
286
+ );
287
+
288
+ const commits = stdout
289
+ .split('\n')
290
+ .filter(line => line.trim())
291
+ .map(line => {
292
+ const [hash, author, email, date, ...messageParts] = line.split('|');
293
+ return {
294
+ hash,
295
+ author,
296
+ email,
297
+ date,
298
+ message: messageParts.join('|')
299
+ };
300
+ });
301
+
302
+ // Get stats for each commit
303
+ for (const commit of commits) {
304
+ try {
305
+ const { stdout: stats } = await execAsync(
306
+ `git show --stat --format='' ${commit.hash}`,
307
+ { cwd: projectPath }
308
+ );
309
+ commit.stats = stats.trim().split('\n').pop(); // Get the summary line
310
+ } catch (error) {
311
+ commit.stats = '';
312
+ }
313
+ }
314
+
315
+ res.json({ commits });
316
+ } catch (error) {
317
+ console.error('Git commits error:', error);
318
+ res.json({ error: error.message });
319
+ }
320
+ });
321
+
322
+ // Get diff for a specific commit
323
+ router.get('/commit-diff', async (req, res) => {
324
+ const { project, commit } = req.query;
325
+
326
+ if (!project || !commit) {
327
+ return res.status(400).json({ error: 'Project name and commit hash are required' });
328
+ }
329
+
330
+ try {
331
+ const projectPath = await getActualProjectPath(project);
332
+
333
+ // Get diff for the commit
334
+ const { stdout } = await execAsync(
335
+ `git show ${commit}`,
336
+ { cwd: projectPath }
337
+ );
338
+
339
+ res.json({ diff: stdout });
340
+ } catch (error) {
341
+ console.error('Git commit diff error:', error);
342
+ res.json({ error: error.message });
343
+ }
344
+ });
345
+
346
+ // Generate commit message based on staged changes
347
+ router.post('/generate-commit-message', async (req, res) => {
348
+ const { project, files } = req.body;
349
+
350
+ if (!project || !files || files.length === 0) {
351
+ return res.status(400).json({ error: 'Project name and files are required' });
352
+ }
353
+
354
+ try {
355
+ const projectPath = await getActualProjectPath(project);
356
+
357
+ // Get diff for selected files
358
+ let combinedDiff = '';
359
+ for (const file of files) {
360
+ try {
361
+ const { stdout } = await execAsync(
362
+ `git diff HEAD -- "${file}"`,
363
+ { cwd: projectPath }
364
+ );
365
+ if (stdout) {
366
+ combinedDiff += `\n--- ${file} ---\n${stdout}`;
367
+ }
368
+ } catch (error) {
369
+ console.error(`Error getting diff for ${file}:`, error);
370
+ }
371
+ }
372
+
373
+ // Use AI to generate commit message (simple implementation)
374
+ // In a real implementation, you might want to use GPT or Claude API
375
+ const message = generateSimpleCommitMessage(files, combinedDiff);
376
+
377
+ res.json({ message });
378
+ } catch (error) {
379
+ console.error('Generate commit message error:', error);
380
+ res.status(500).json({ error: error.message });
381
+ }
382
+ });
383
+
384
+ // Simple commit message generator (can be replaced with AI)
385
+ function generateSimpleCommitMessage(files, diff) {
386
+ const fileCount = files.length;
387
+ const isMultipleFiles = fileCount > 1;
388
+
389
+ // Analyze the diff to determine the type of change
390
+ const additions = (diff.match(/^\+[^+]/gm) || []).length;
391
+ const deletions = (diff.match(/^-[^-]/gm) || []).length;
392
+
393
+ // Determine the primary action
394
+ let action = 'Update';
395
+ if (additions > 0 && deletions === 0) {
396
+ action = 'Add';
397
+ } else if (deletions > 0 && additions === 0) {
398
+ action = 'Remove';
399
+ } else if (additions > deletions * 2) {
400
+ action = 'Enhance';
401
+ } else if (deletions > additions * 2) {
402
+ action = 'Refactor';
403
+ }
404
+
405
+ // Generate message based on files
406
+ if (isMultipleFiles) {
407
+ const components = new Set(files.map(f => {
408
+ const parts = f.split('/');
409
+ return parts[parts.length - 2] || parts[0];
410
+ }));
411
+
412
+ if (components.size === 1) {
413
+ return `${action} ${[...components][0]} component`;
414
+ } else {
415
+ return `${action} multiple components`;
416
+ }
417
+ } else {
418
+ const fileName = files[0].split('/').pop();
419
+ const componentName = fileName.replace(/\.(jsx?|tsx?|css|scss)$/, '');
420
+ return `${action} ${componentName}`;
421
+ }
422
+ }
423
+
424
+ // Get remote status (ahead/behind commits with smart remote detection)
425
+ router.get('/remote-status', async (req, res) => {
426
+ const { project } = req.query;
427
+
428
+ if (!project) {
429
+ return res.status(400).json({ error: 'Project name is required' });
430
+ }
431
+
432
+ try {
433
+ const projectPath = await getActualProjectPath(project);
434
+ await validateGitRepository(projectPath);
435
+
436
+ // Get current branch
437
+ const { stdout: currentBranch } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
438
+ const branch = currentBranch.trim();
439
+
440
+ // Check if there's a remote tracking branch (smart detection)
441
+ let trackingBranch;
442
+ let remoteName;
443
+ try {
444
+ const { stdout } = await execAsync(`git rev-parse --abbrev-ref ${branch}@{upstream}`, { cwd: projectPath });
445
+ trackingBranch = stdout.trim();
446
+ remoteName = trackingBranch.split('/')[0]; // Extract remote name (e.g., "origin/main" -> "origin")
447
+ } catch (error) {
448
+ // No upstream branch configured - but check if we have remotes
449
+ let hasRemote = false;
450
+ let remoteName = null;
451
+ try {
452
+ const { stdout } = await execAsync('git remote', { cwd: projectPath });
453
+ const remotes = stdout.trim().split('\n').filter(r => r.trim());
454
+ if (remotes.length > 0) {
455
+ hasRemote = true;
456
+ remoteName = remotes.includes('origin') ? 'origin' : remotes[0];
457
+ }
458
+ } catch (remoteError) {
459
+ // No remotes configured
460
+ }
461
+
462
+ return res.json({
463
+ hasRemote,
464
+ hasUpstream: false,
465
+ branch,
466
+ remoteName,
467
+ message: 'No remote tracking branch configured'
468
+ });
469
+ }
470
+
471
+ // Get ahead/behind counts
472
+ const { stdout: countOutput } = await execAsync(
473
+ `git rev-list --count --left-right ${trackingBranch}...HEAD`,
474
+ { cwd: projectPath }
475
+ );
476
+
477
+ const [behind, ahead] = countOutput.trim().split('\t').map(Number);
478
+
479
+ res.json({
480
+ hasRemote: true,
481
+ hasUpstream: true,
482
+ branch,
483
+ remoteBranch: trackingBranch,
484
+ remoteName,
485
+ ahead: ahead || 0,
486
+ behind: behind || 0,
487
+ isUpToDate: ahead === 0 && behind === 0
488
+ });
489
+ } catch (error) {
490
+ console.error('Git remote status error:', error);
491
+ res.json({ error: error.message });
492
+ }
493
+ });
494
+
495
+ // Fetch from remote (using smart remote detection)
496
+ router.post('/fetch', async (req, res) => {
497
+ const { project } = req.body;
498
+
499
+ if (!project) {
500
+ return res.status(400).json({ error: 'Project name is required' });
501
+ }
502
+
503
+ try {
504
+ const projectPath = await getActualProjectPath(project);
505
+ await validateGitRepository(projectPath);
506
+
507
+ // Get current branch and its upstream remote
508
+ const { stdout: currentBranch } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
509
+ const branch = currentBranch.trim();
510
+
511
+ let remoteName = 'origin'; // fallback
512
+ try {
513
+ const { stdout } = await execAsync(`git rev-parse --abbrev-ref ${branch}@{upstream}`, { cwd: projectPath });
514
+ remoteName = stdout.trim().split('/')[0]; // Extract remote name
515
+ } catch (error) {
516
+ // No upstream, try to fetch from origin anyway
517
+ console.log('No upstream configured, using origin as fallback');
518
+ }
519
+
520
+ const { stdout } = await execAsync(`git fetch ${remoteName}`, { cwd: projectPath });
521
+
522
+ res.json({ success: true, output: stdout || 'Fetch completed successfully', remoteName });
523
+ } catch (error) {
524
+ console.error('Git fetch error:', error);
525
+ res.status(500).json({
526
+ error: 'Fetch failed',
527
+ details: error.message.includes('Could not resolve hostname')
528
+ ? 'Unable to connect to remote repository. Check your internet connection.'
529
+ : error.message.includes('fatal: \'origin\' does not appear to be a git repository')
530
+ ? 'No remote repository configured. Add a remote with: git remote add origin <url>'
531
+ : error.message
532
+ });
533
+ }
534
+ });
535
+
536
+ // Pull from remote (fetch + merge using smart remote detection)
537
+ router.post('/pull', async (req, res) => {
538
+ const { project } = req.body;
539
+
540
+ if (!project) {
541
+ return res.status(400).json({ error: 'Project name is required' });
542
+ }
543
+
544
+ try {
545
+ const projectPath = await getActualProjectPath(project);
546
+ await validateGitRepository(projectPath);
547
+
548
+ // Get current branch and its upstream remote
549
+ const { stdout: currentBranch } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
550
+ const branch = currentBranch.trim();
551
+
552
+ let remoteName = 'origin'; // fallback
553
+ let remoteBranch = branch; // fallback
554
+ try {
555
+ const { stdout } = await execAsync(`git rev-parse --abbrev-ref ${branch}@{upstream}`, { cwd: projectPath });
556
+ const tracking = stdout.trim();
557
+ remoteName = tracking.split('/')[0]; // Extract remote name
558
+ remoteBranch = tracking.split('/').slice(1).join('/'); // Extract branch name
559
+ } catch (error) {
560
+ // No upstream, use fallback
561
+ console.log('No upstream configured, using origin/branch as fallback');
562
+ }
563
+
564
+ const { stdout } = await execAsync(`git pull ${remoteName} ${remoteBranch}`, { cwd: projectPath });
565
+
566
+ res.json({
567
+ success: true,
568
+ output: stdout || 'Pull completed successfully',
569
+ remoteName,
570
+ remoteBranch
571
+ });
572
+ } catch (error) {
573
+ console.error('Git pull error:', error);
574
+
575
+ // Enhanced error handling for common pull scenarios
576
+ let errorMessage = 'Pull failed';
577
+ let details = error.message;
578
+
579
+ if (error.message.includes('CONFLICT')) {
580
+ errorMessage = 'Merge conflicts detected';
581
+ details = 'Pull created merge conflicts. Please resolve conflicts manually in the editor, then commit the changes.';
582
+ } else if (error.message.includes('Please commit your changes or stash them')) {
583
+ errorMessage = 'Uncommitted changes detected';
584
+ details = 'Please commit or stash your local changes before pulling.';
585
+ } else if (error.message.includes('Could not resolve hostname')) {
586
+ errorMessage = 'Network error';
587
+ details = 'Unable to connect to remote repository. Check your internet connection.';
588
+ } else if (error.message.includes('fatal: \'origin\' does not appear to be a git repository')) {
589
+ errorMessage = 'Remote not configured';
590
+ details = 'No remote repository configured. Add a remote with: git remote add origin <url>';
591
+ } else if (error.message.includes('diverged')) {
592
+ errorMessage = 'Branches have diverged';
593
+ details = 'Your local branch and remote branch have diverged. Consider fetching first to review changes.';
594
+ }
595
+
596
+ res.status(500).json({
597
+ error: errorMessage,
598
+ details: details
599
+ });
600
+ }
601
+ });
602
+
603
+ // Push commits to remote repository
604
+ router.post('/push', async (req, res) => {
605
+ const { project } = req.body;
606
+
607
+ if (!project) {
608
+ return res.status(400).json({ error: 'Project name is required' });
609
+ }
610
+
611
+ try {
612
+ const projectPath = await getActualProjectPath(project);
613
+ await validateGitRepository(projectPath);
614
+
615
+ // Get current branch and its upstream remote
616
+ const { stdout: currentBranch } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
617
+ const branch = currentBranch.trim();
618
+
619
+ let remoteName = 'origin'; // fallback
620
+ let remoteBranch = branch; // fallback
621
+ try {
622
+ const { stdout } = await execAsync(`git rev-parse --abbrev-ref ${branch}@{upstream}`, { cwd: projectPath });
623
+ const tracking = stdout.trim();
624
+ remoteName = tracking.split('/')[0]; // Extract remote name
625
+ remoteBranch = tracking.split('/').slice(1).join('/'); // Extract branch name
626
+ } catch (error) {
627
+ // No upstream, use fallback
628
+ console.log('No upstream configured, using origin/branch as fallback');
629
+ }
630
+
631
+ const { stdout } = await execAsync(`git push ${remoteName} ${remoteBranch}`, { cwd: projectPath });
632
+
633
+ res.json({
634
+ success: true,
635
+ output: stdout || 'Push completed successfully',
636
+ remoteName,
637
+ remoteBranch
638
+ });
639
+ } catch (error) {
640
+ console.error('Git push error:', error);
641
+
642
+ // Enhanced error handling for common push scenarios
643
+ let errorMessage = 'Push failed';
644
+ let details = error.message;
645
+
646
+ if (error.message.includes('rejected')) {
647
+ errorMessage = 'Push rejected';
648
+ details = 'The remote has newer commits. Pull first to merge changes before pushing.';
649
+ } else if (error.message.includes('non-fast-forward')) {
650
+ errorMessage = 'Non-fast-forward push';
651
+ details = 'Your branch is behind the remote. Pull the latest changes first.';
652
+ } else if (error.message.includes('Could not resolve hostname')) {
653
+ errorMessage = 'Network error';
654
+ details = 'Unable to connect to remote repository. Check your internet connection.';
655
+ } else if (error.message.includes('fatal: \'origin\' does not appear to be a git repository')) {
656
+ errorMessage = 'Remote not configured';
657
+ details = 'No remote repository configured. Add a remote with: git remote add origin <url>';
658
+ } else if (error.message.includes('Permission denied')) {
659
+ errorMessage = 'Authentication failed';
660
+ details = 'Permission denied. Check your credentials or SSH keys.';
661
+ } else if (error.message.includes('no upstream branch')) {
662
+ errorMessage = 'No upstream branch';
663
+ details = 'No upstream branch configured. Use: git push --set-upstream origin <branch>';
664
+ }
665
+
666
+ res.status(500).json({
667
+ error: errorMessage,
668
+ details: details
669
+ });
670
+ }
671
+ });
672
+
673
+ // Publish branch to remote (set upstream and push)
674
+ router.post('/publish', async (req, res) => {
675
+ const { project, branch } = req.body;
676
+
677
+ if (!project || !branch) {
678
+ return res.status(400).json({ error: 'Project name and branch are required' });
679
+ }
680
+
681
+ try {
682
+ const projectPath = await getActualProjectPath(project);
683
+ await validateGitRepository(projectPath);
684
+
685
+ // Get current branch to verify it matches the requested branch
686
+ const { stdout: currentBranch } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
687
+ const currentBranchName = currentBranch.trim();
688
+
689
+ if (currentBranchName !== branch) {
690
+ return res.status(400).json({
691
+ error: `Branch mismatch. Current branch is ${currentBranchName}, but trying to publish ${branch}`
692
+ });
693
+ }
694
+
695
+ // Check if remote exists
696
+ let remoteName = 'origin';
697
+ try {
698
+ const { stdout } = await execAsync('git remote', { cwd: projectPath });
699
+ const remotes = stdout.trim().split('\n').filter(r => r.trim());
700
+ if (remotes.length === 0) {
701
+ return res.status(400).json({
702
+ error: 'No remote repository configured. Add a remote with: git remote add origin <url>'
703
+ });
704
+ }
705
+ remoteName = remotes.includes('origin') ? 'origin' : remotes[0];
706
+ } catch (error) {
707
+ return res.status(400).json({
708
+ error: 'No remote repository configured. Add a remote with: git remote add origin <url>'
709
+ });
710
+ }
711
+
712
+ // Publish the branch (set upstream and push)
713
+ const { stdout } = await execAsync(`git push --set-upstream ${remoteName} ${branch}`, { cwd: projectPath });
714
+
715
+ res.json({
716
+ success: true,
717
+ output: stdout || 'Branch published successfully',
718
+ remoteName,
719
+ branch
720
+ });
721
+ } catch (error) {
722
+ console.error('Git publish error:', error);
723
+
724
+ // Enhanced error handling for common publish scenarios
725
+ let errorMessage = 'Publish failed';
726
+ let details = error.message;
727
+
728
+ if (error.message.includes('rejected')) {
729
+ errorMessage = 'Publish rejected';
730
+ details = 'The remote branch already exists and has different commits. Use push instead.';
731
+ } else if (error.message.includes('Could not resolve hostname')) {
732
+ errorMessage = 'Network error';
733
+ details = 'Unable to connect to remote repository. Check your internet connection.';
734
+ } else if (error.message.includes('Permission denied')) {
735
+ errorMessage = 'Authentication failed';
736
+ details = 'Permission denied. Check your credentials or SSH keys.';
737
+ } else if (error.message.includes('fatal:') && error.message.includes('does not appear to be a git repository')) {
738
+ errorMessage = 'Remote not configured';
739
+ details = 'Remote repository not properly configured. Check your remote URL.';
740
+ }
741
+
742
+ res.status(500).json({
743
+ error: errorMessage,
744
+ details: details
745
+ });
746
+ }
747
+ });
748
+
749
+ // Discard changes for a specific file
750
+ router.post('/discard', async (req, res) => {
751
+ const { project, file } = req.body;
752
+
753
+ if (!project || !file) {
754
+ return res.status(400).json({ error: 'Project name and file path are required' });
755
+ }
756
+
757
+ try {
758
+ const projectPath = await getActualProjectPath(project);
759
+ await validateGitRepository(projectPath);
760
+
761
+ // Check file status to determine correct discard command
762
+ const { stdout: statusOutput } = await execAsync(`git status --porcelain "${file}"`, { cwd: projectPath });
763
+
764
+ if (!statusOutput.trim()) {
765
+ return res.status(400).json({ error: 'No changes to discard for this file' });
766
+ }
767
+
768
+ const status = statusOutput.substring(0, 2);
769
+
770
+ if (status === '??') {
771
+ // Untracked file - delete it
772
+ await fs.unlink(path.join(projectPath, file));
773
+ } else if (status.includes('M') || status.includes('D')) {
774
+ // Modified or deleted file - restore from HEAD
775
+ await execAsync(`git restore "${file}"`, { cwd: projectPath });
776
+ } else if (status.includes('A')) {
777
+ // Added file - unstage it
778
+ await execAsync(`git reset HEAD "${file}"`, { cwd: projectPath });
779
+ }
780
+
781
+ res.json({ success: true, message: `Changes discarded for ${file}` });
782
+ } catch (error) {
783
+ console.error('Git discard error:', error);
784
+ res.status(500).json({ error: error.message });
785
+ }
786
+ });
787
+
788
+ // Delete untracked file
789
+ router.post('/delete-untracked', async (req, res) => {
790
+ const { project, file } = req.body;
791
+
792
+ if (!project || !file) {
793
+ return res.status(400).json({ error: 'Project name and file path are required' });
794
+ }
795
+
796
+ try {
797
+ const projectPath = await getActualProjectPath(project);
798
+ await validateGitRepository(projectPath);
799
+
800
+ // Check if file is actually untracked
801
+ const { stdout: statusOutput } = await execAsync(`git status --porcelain "${file}"`, { cwd: projectPath });
802
+
803
+ if (!statusOutput.trim()) {
804
+ return res.status(400).json({ error: 'File is not untracked or does not exist' });
805
+ }
806
+
807
+ const status = statusOutput.substring(0, 2);
808
+
809
+ if (status !== '??') {
810
+ return res.status(400).json({ error: 'File is not untracked. Use discard for tracked files.' });
811
+ }
812
+
813
+ // Delete the untracked file
814
+ await fs.unlink(path.join(projectPath, file));
815
+
816
+ res.json({ success: true, message: `Untracked file ${file} deleted successfully` });
817
+ } catch (error) {
818
+ console.error('Git delete untracked error:', error);
819
+ res.status(500).json({ error: error.message });
820
+ }
821
+ });
822
+
823
+ export default router;