@axhub/genie 0.2.8 → 0.2.10

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/LICENSE +21 -675
  2. package/dist/api-docs.html +2 -2
  3. package/dist/assets/App-CYCCsgwf.js +264 -0
  4. package/dist/assets/ReviewApp-0srHIXwb.js +1 -0
  5. package/dist/assets/{_basePickBy-CqJbRZ9y.js → _basePickBy-DVVb07UV.js} +1 -1
  6. package/dist/assets/{_baseUniq-BS8YH8jO.js → _baseUniq-BtbziL5G.js} +1 -1
  7. package/dist/assets/{arc-BBmKEN-S.js → arc-BsCC8yBD.js} +1 -1
  8. package/dist/assets/{architectureDiagram-2XIMDMQ5-N5lcb82R.js → architectureDiagram-2XIMDMQ5-woFp6eNI.js} +1 -1
  9. package/dist/assets/{blockDiagram-WCTKOSBZ-DTMwHuLn.js → blockDiagram-WCTKOSBZ-ya8VAc2k.js} +1 -1
  10. package/dist/assets/{c4Diagram-IC4MRINW-BTKlkXI9.js → c4Diagram-IC4MRINW-CY1dZmIZ.js} +1 -1
  11. package/dist/assets/channel-BMhScXFe.js +1 -0
  12. package/dist/assets/{chunk-4BX2VUAB-DUdoTxAc.js → chunk-4BX2VUAB-CR1lAd74.js} +1 -1
  13. package/dist/assets/{chunk-55IACEB6-Bm_92xe4.js → chunk-55IACEB6-CP98WcFC.js} +1 -1
  14. package/dist/assets/{chunk-FMBD7UC4-CGW0g62g.js → chunk-FMBD7UC4-D9c7ijAB.js} +1 -1
  15. package/dist/assets/{chunk-JSJVCQXG-DYkTH3w1.js → chunk-JSJVCQXG-DQAGYOn-.js} +1 -1
  16. package/dist/assets/{chunk-KX2RTZJC-C9oTlISU.js → chunk-KX2RTZJC-BbTXiDq7.js} +1 -1
  17. package/dist/assets/{chunk-NQ4KR5QH-CM50ygWP.js → chunk-NQ4KR5QH-BI6AX0dr.js} +1 -1
  18. package/dist/assets/{chunk-QZHKN3VN-7dzpYeNJ.js → chunk-QZHKN3VN-DB3V2Ifo.js} +1 -1
  19. package/dist/assets/{chunk-WL4C6EOR-Cm9nQrsr.js → chunk-WL4C6EOR-DhzTthv6.js} +1 -1
  20. package/dist/assets/classDiagram-VBA2DB6C-CMIxlWcT.js +1 -0
  21. package/dist/assets/classDiagram-v2-RAHNMMFH-CMIxlWcT.js +1 -0
  22. package/dist/assets/clone-BPqOt4r3.js +1 -0
  23. package/dist/assets/{cose-bilkent-S5V4N54A-Ccp_p0JZ.js → cose-bilkent-S5V4N54A-BQ09ZE2j.js} +1 -1
  24. package/dist/assets/{dagre-KLK3FWXG-fBwTLUp9.js → dagre-KLK3FWXG-Dc2ueD_R.js} +1 -1
  25. package/dist/assets/{diagram-E7M64L7V-CeNVmFUp.js → diagram-E7M64L7V-DP-LsQoL.js} +1 -1
  26. package/dist/assets/{diagram-IFDJBPK2-CtavyLGa.js → diagram-IFDJBPK2-Cg6r42cB.js} +1 -1
  27. package/dist/assets/{diagram-P4PSJMXO-CpQTjQwc.js → diagram-P4PSJMXO-aHsfoUZE.js} +1 -1
  28. package/dist/assets/{erDiagram-INFDFZHY-B8R5vwhd.js → erDiagram-INFDFZHY-qBXJ4aAz.js} +1 -1
  29. package/dist/assets/{flowDiagram-PKNHOUZH-BvkVVwIQ.js → flowDiagram-PKNHOUZH-D_13emJM.js} +1 -1
  30. package/dist/assets/{ganttDiagram-A5KZAMGK-DOu3hSNa.js → ganttDiagram-A5KZAMGK-BvIcOLwz.js} +1 -1
  31. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-C7zT67YE.js → gitGraphDiagram-K3NZZRJ6-ad0vvNcU.js} +1 -1
  32. package/dist/assets/{graph-D11wiwHo.js → graph-CeJCMjan.js} +1 -1
  33. package/dist/assets/{highlighted-body-TPN3WLV5-Babpthg-.js → highlighted-body-TPN3WLV5-B_novwSz.js} +1 -1
  34. package/dist/assets/index-C514cLyb.js +2 -0
  35. package/dist/assets/index-h1DBl_g3.css +1 -0
  36. package/dist/assets/{infoDiagram-LFFYTUFH-BmA7IpQG.js → infoDiagram-LFFYTUFH-lOxAqb3m.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-PHBUUO56-BEquZd3E.js → ishikawaDiagram-PHBUUO56-DIr-51gj.js} +1 -1
  38. package/dist/assets/{journeyDiagram-4ABVD52K-BfemGz7f.js → journeyDiagram-4ABVD52K-CYcIW0ZU.js} +1 -1
  39. package/dist/assets/{kanban-definition-K7BYSVSG-CWja3mln.js → kanban-definition-K7BYSVSG-C1ZK616a.js} +1 -1
  40. package/dist/assets/{layout-BLUNf-PJ.js → layout-CI2RM-v6.js} +1 -1
  41. package/dist/assets/{linear-DukIV_Xv.js → linear-DE7bISck.js} +1 -1
  42. package/dist/assets/{mermaid-O7DHMXV3-SgtM28qI.js → mermaid-O7DHMXV3-XxAJo8EK.js} +6 -6
  43. package/dist/assets/{mindmap-definition-YRQLILUH-4UjqXITU.js → mindmap-definition-YRQLILUH-Dz6EFjmn.js} +1 -1
  44. package/dist/assets/{pieDiagram-SKSYHLDU-8AxqJd0M.js → pieDiagram-SKSYHLDU-DPpEzUed.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-337W2JSQ-D60m8V8r.js → quadrantDiagram-337W2JSQ-xdoXNet7.js} +1 -1
  46. package/dist/assets/{requirementDiagram-Z7DCOOCP-zqh9jBVf.js → requirementDiagram-Z7DCOOCP-DUq8H3CL.js} +1 -1
  47. package/dist/assets/{sankeyDiagram-WA2Y5GQK-CDZILTLI.js → sankeyDiagram-WA2Y5GQK-CmqEUxRu.js} +1 -1
  48. package/dist/assets/{sequenceDiagram-2WXFIKYE-7BReFd0L.js → sequenceDiagram-2WXFIKYE-DhtXRNiH.js} +1 -1
  49. package/dist/assets/{stateDiagram-RAJIS63D-HPTVdIG4.js → stateDiagram-RAJIS63D-Dj0HOlbN.js} +1 -1
  50. package/dist/assets/stateDiagram-v2-FVOUBMTO-C9utf5gv.js +1 -0
  51. package/dist/assets/{timeline-definition-YZTLITO2-CTVllFgr.js → timeline-definition-YZTLITO2-DUuJzZB5.js} +1 -1
  52. package/dist/assets/{treemap-KZPCXAKY-BtyxboJZ.js → treemap-KZPCXAKY-DpYBQ0qr.js} +1 -1
  53. package/dist/assets/vendor-codemirror-CMHSJ_9p.js +9 -0
  54. package/dist/assets/{vendor-react-Cpt6D04s.js → vendor-react-xmA_f8ig.js} +1 -1
  55. package/dist/assets/{vennDiagram-LZ73GAT5-D96ZI6Mg.js → vennDiagram-LZ73GAT5-DpePUyOd.js} +1 -1
  56. package/dist/assets/{xychartDiagram-JWTSCODW-eRk-39YO.js → xychartDiagram-JWTSCODW-Cfp1I4_U.js} +1 -1
  57. package/dist/index.html +5 -5
  58. package/package.json +8 -7
  59. package/server/acp-runtime/client.js +129 -16
  60. package/server/acp-runtime/index.js +54 -0
  61. package/server/acp-runtime/registry.js +2 -2
  62. package/server/acp-runtime/session-store.js +79 -5
  63. package/server/cli.js +55 -10
  64. package/server/database/db.js +20 -0
  65. package/server/external-agent/service.js +24 -6
  66. package/server/external-agent/ws.js +540 -27
  67. package/server/index.js +112 -151
  68. package/server/lan-access/core.js +79 -0
  69. package/server/lan-access/state.js +102 -0
  70. package/server/middleware/auth.js +57 -14
  71. package/server/projects.js +930 -667
  72. package/server/routes/auth.js +24 -4
  73. package/server/routes/cli-auth.js +21 -25
  74. package/server/routes/codex.js +84 -298
  75. package/server/routes/commands.js +322 -407
  76. package/server/routes/lan-access.js +231 -0
  77. package/server/routes/projects.js +154 -158
  78. package/server/routes/session-core.js +160 -91
  79. package/server/routes/settings.js +113 -99
  80. package/server/session-core/eventStore.js +60 -20
  81. package/server/session-core/providerAdapters.js +75 -38
  82. package/server/session-core/runtimeState.js +8 -0
  83. package/server/session-core/sessionListMerge.js +47 -0
  84. package/shared/conversationEvents.js +174 -15
  85. package/shared/modelConstants.js +79 -99
  86. package/dist/assets/App-CTKZtqB1.js +0 -460
  87. package/dist/assets/ReviewApp-DM6BNAzR.js +0 -1
  88. package/dist/assets/channel-1oJBvF-0.js +0 -1
  89. package/dist/assets/classDiagram-VBA2DB6C-d5TeKFM4.js +0 -1
  90. package/dist/assets/classDiagram-v2-RAHNMMFH-d5TeKFM4.js +0 -1
  91. package/dist/assets/clone-CinxIlEu.js +0 -1
  92. package/dist/assets/index-DFxzgWoO.js +0 -2
  93. package/dist/assets/index-YCFGDVKw.css +0 -1
  94. package/dist/assets/stateDiagram-v2-FVOUBMTO-DTUf5_gC.js +0 -1
  95. package/dist/assets/vendor-codemirror-Dz7_EqNA.js +0 -39
  96. package/server/_legacy-providers/README.md +0 -30
  97. package/server/_legacy-providers/claude-sdk.js +0 -956
  98. package/server/_legacy-providers/gemini-cli.js +0 -368
  99. package/server/_legacy-providers/openai-codex.js +0 -705
  100. package/server/_legacy-providers/opencode-cli.js +0 -674
  101. package/server/routes/git.js +0 -1110
  102. package/server/routes/mcp-utils.js +0 -48
  103. package/server/routes/mcp.js +0 -536
  104. package/server/routes/taskmaster.js +0 -1963
  105. package/server/utils/mcp-detector.js +0 -198
  106. package/server/utils/taskmaster-websocket.js +0 -129
@@ -1,1110 +0,0 @@
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 { queryGemini } from '../gemini-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', 'gemini'].includes(provider)) {
523
- return res.status(400).json({ error: 'provider must be "claude" or "gemini"' });
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, Codex, Gemini, or OpenCode)
577
- * @param {Array<string>} files - List of changed files
578
- * @param {string} diffContext - Git diff content
579
- * @param {string} provider - 'claude' or 'gemini'
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
- if (parsed.type === 'conversation-event' && parsed.event?.kind === 'assistant_text_delta' && parsed.event?.payload?.text) {
614
- responseText += parsed.event.payload.text;
615
- } else if (parsed.type === 'text' && parsed.text) {
616
- console.log('✅ Direct text:', parsed.text.substring(0, 100));
617
- responseText += parsed.text;
618
- }
619
- } catch (e) {
620
- // Ignore parse errors
621
- console.error('Error parsing writer data:', e);
622
- }
623
- },
624
- setSessionId: () => {}, // No-op for this use case
625
- };
626
-
627
- console.log('🚀 Calling AI agent with provider:', provider);
628
- console.log('📝 Prompt length:', prompt.length);
629
-
630
- // Call the appropriate agent
631
- if (provider === 'claude') {
632
- await queryClaudeSDK(prompt, {
633
- cwd: projectPath,
634
- permissionMode: 'bypassPermissions',
635
- model: 'sonnet'
636
- }, writer);
637
- } else if (provider === 'gemini') {
638
- await queryGemini(prompt, {
639
- cwd: projectPath,
640
- projectPath,
641
- permissionMode: 'bypassPermissions'
642
- }, writer);
643
- }
644
-
645
- console.log('📊 Total response text collected:', responseText.length, 'characters');
646
- console.log('📄 Response preview:', responseText.substring(0, 200));
647
-
648
- // Clean up the response
649
- const cleanedMessage = cleanCommitMessage(responseText);
650
- console.log('🧹 Cleaned message:', cleanedMessage.substring(0, 200));
651
-
652
- return cleanedMessage || 'chore: update files';
653
- } catch (error) {
654
- console.error('Error generating commit message with AI:', error);
655
- // Fallback to simple message
656
- return `chore: update ${files.length} file${files.length !== 1 ? 's' : ''}`;
657
- }
658
- }
659
-
660
- /**
661
- * Cleans the AI-generated commit message by removing markdown, code blocks, and extra formatting
662
- * @param {string} text - Raw AI response
663
- * @returns {string} Clean commit message
664
- */
665
- function cleanCommitMessage(text) {
666
- if (!text || !text.trim()) {
667
- return '';
668
- }
669
-
670
- let cleaned = text.trim();
671
-
672
- // Remove markdown code blocks
673
- cleaned = cleaned.replace(/```[a-z]*\n/g, '');
674
- cleaned = cleaned.replace(/```/g, '');
675
-
676
- // Remove markdown headers
677
- cleaned = cleaned.replace(/^#+\s*/gm, '');
678
-
679
- // Remove leading/trailing quotes
680
- cleaned = cleaned.replace(/^["']|["']$/g, '');
681
-
682
- // If there are multiple lines, take everything (subject + body)
683
- // Just clean up extra blank lines
684
- cleaned = cleaned.replace(/\n{3,}/g, '\n\n');
685
-
686
- // Remove any explanatory text before the actual commit message
687
- // Look for conventional commit pattern and start from there
688
- const conventionalCommitMatch = cleaned.match(/(feat|fix|docs|style|refactor|perf|test|build|ci|chore)(\(.+?\))?:.+/s);
689
- if (conventionalCommitMatch) {
690
- cleaned = cleaned.substring(cleaned.indexOf(conventionalCommitMatch[0]));
691
- }
692
-
693
- return cleaned.trim();
694
- }
695
-
696
- // Get remote status (ahead/behind commits with smart remote detection)
697
- router.get('/remote-status', async (req, res) => {
698
- const { project } = req.query;
699
-
700
- if (!project) {
701
- return res.status(400).json({ error: 'Project name is required' });
702
- }
703
-
704
- try {
705
- const projectPath = await getActualProjectPath(project);
706
- await validateGitRepository(projectPath);
707
-
708
- // Get current branch
709
- const { stdout: currentBranch } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
710
- const branch = currentBranch.trim();
711
-
712
- // Check if there's a remote tracking branch (smart detection)
713
- let trackingBranch;
714
- let remoteName;
715
- try {
716
- const { stdout } = await execAsync(`git rev-parse --abbrev-ref ${branch}@{upstream}`, { cwd: projectPath });
717
- trackingBranch = stdout.trim();
718
- remoteName = trackingBranch.split('/')[0]; // Extract remote name (e.g., "origin/main" -> "origin")
719
- } catch (error) {
720
- // No upstream branch configured - but check if we have remotes
721
- let hasRemote = false;
722
- let remoteName = null;
723
- try {
724
- const { stdout } = await execAsync('git remote', { cwd: projectPath });
725
- const remotes = stdout.trim().split('\n').filter(r => r.trim());
726
- if (remotes.length > 0) {
727
- hasRemote = true;
728
- remoteName = remotes.includes('origin') ? 'origin' : remotes[0];
729
- }
730
- } catch (remoteError) {
731
- // No remotes configured
732
- }
733
-
734
- return res.json({
735
- hasRemote,
736
- hasUpstream: false,
737
- branch,
738
- remoteName,
739
- message: 'No remote tracking branch configured'
740
- });
741
- }
742
-
743
- // Get ahead/behind counts
744
- const { stdout: countOutput } = await execAsync(
745
- `git rev-list --count --left-right ${trackingBranch}...HEAD`,
746
- { cwd: projectPath }
747
- );
748
-
749
- const [behind, ahead] = countOutput.trim().split('\t').map(Number);
750
-
751
- res.json({
752
- hasRemote: true,
753
- hasUpstream: true,
754
- branch,
755
- remoteBranch: trackingBranch,
756
- remoteName,
757
- ahead: ahead || 0,
758
- behind: behind || 0,
759
- isUpToDate: ahead === 0 && behind === 0
760
- });
761
- } catch (error) {
762
- console.error('Git remote status error:', error);
763
- res.json({ error: error.message });
764
- }
765
- });
766
-
767
- // Fetch from remote (using smart remote detection)
768
- router.post('/fetch', async (req, res) => {
769
- const { project } = req.body;
770
-
771
- if (!project) {
772
- return res.status(400).json({ error: 'Project name is required' });
773
- }
774
-
775
- try {
776
- const projectPath = await getActualProjectPath(project);
777
- await validateGitRepository(projectPath);
778
-
779
- // Get current branch and its upstream remote
780
- const { stdout: currentBranch } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
781
- const branch = currentBranch.trim();
782
-
783
- let remoteName = 'origin'; // fallback
784
- try {
785
- const { stdout } = await execAsync(`git rev-parse --abbrev-ref ${branch}@{upstream}`, { cwd: projectPath });
786
- remoteName = stdout.trim().split('/')[0]; // Extract remote name
787
- } catch (error) {
788
- // No upstream, try to fetch from origin anyway
789
- console.log('No upstream configured, using origin as fallback');
790
- }
791
-
792
- const { stdout } = await execAsync(`git fetch ${remoteName}`, { cwd: projectPath });
793
-
794
- res.json({ success: true, output: stdout || 'Fetch completed successfully', remoteName });
795
- } catch (error) {
796
- console.error('Git fetch error:', error);
797
- res.status(500).json({
798
- error: 'Fetch failed',
799
- details: error.message.includes('Could not resolve hostname')
800
- ? 'Unable to connect to remote repository. Check your internet connection.'
801
- : error.message.includes('fatal: \'origin\' does not appear to be a git repository')
802
- ? 'No remote repository configured. Add a remote with: git remote add origin <url>'
803
- : error.message
804
- });
805
- }
806
- });
807
-
808
- // Pull from remote (fetch + merge using smart remote detection)
809
- router.post('/pull', async (req, res) => {
810
- const { project } = req.body;
811
-
812
- if (!project) {
813
- return res.status(400).json({ error: 'Project name is required' });
814
- }
815
-
816
- try {
817
- const projectPath = await getActualProjectPath(project);
818
- await validateGitRepository(projectPath);
819
-
820
- // Get current branch and its upstream remote
821
- const { stdout: currentBranch } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
822
- const branch = currentBranch.trim();
823
-
824
- let remoteName = 'origin'; // fallback
825
- let remoteBranch = branch; // fallback
826
- try {
827
- const { stdout } = await execAsync(`git rev-parse --abbrev-ref ${branch}@{upstream}`, { cwd: projectPath });
828
- const tracking = stdout.trim();
829
- remoteName = tracking.split('/')[0]; // Extract remote name
830
- remoteBranch = tracking.split('/').slice(1).join('/'); // Extract branch name
831
- } catch (error) {
832
- // No upstream, use fallback
833
- console.log('No upstream configured, using origin/branch as fallback');
834
- }
835
-
836
- const { stdout } = await execAsync(`git pull ${remoteName} ${remoteBranch}`, { cwd: projectPath });
837
-
838
- res.json({
839
- success: true,
840
- output: stdout || 'Pull completed successfully',
841
- remoteName,
842
- remoteBranch
843
- });
844
- } catch (error) {
845
- console.error('Git pull error:', error);
846
-
847
- // Enhanced error handling for common pull scenarios
848
- let errorMessage = 'Pull failed';
849
- let details = error.message;
850
-
851
- if (error.message.includes('CONFLICT')) {
852
- errorMessage = 'Merge conflicts detected';
853
- details = 'Pull created merge conflicts. Please resolve conflicts manually in the editor, then commit the changes.';
854
- } else if (error.message.includes('Please commit your changes or stash them')) {
855
- errorMessage = 'Uncommitted changes detected';
856
- details = 'Please commit or stash your local changes before pulling.';
857
- } else if (error.message.includes('Could not resolve hostname')) {
858
- errorMessage = 'Network error';
859
- details = 'Unable to connect to remote repository. Check your internet connection.';
860
- } else if (error.message.includes('fatal: \'origin\' does not appear to be a git repository')) {
861
- errorMessage = 'Remote not configured';
862
- details = 'No remote repository configured. Add a remote with: git remote add origin <url>';
863
- } else if (error.message.includes('diverged')) {
864
- errorMessage = 'Branches have diverged';
865
- details = 'Your local branch and remote branch have diverged. Consider fetching first to review changes.';
866
- }
867
-
868
- res.status(500).json({
869
- error: errorMessage,
870
- details: details
871
- });
872
- }
873
- });
874
-
875
- // Push commits to remote repository
876
- router.post('/push', async (req, res) => {
877
- const { project } = req.body;
878
-
879
- if (!project) {
880
- return res.status(400).json({ error: 'Project name is required' });
881
- }
882
-
883
- try {
884
- const projectPath = await getActualProjectPath(project);
885
- await validateGitRepository(projectPath);
886
-
887
- // Get current branch and its upstream remote
888
- const { stdout: currentBranch } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
889
- const branch = currentBranch.trim();
890
-
891
- let remoteName = 'origin'; // fallback
892
- let remoteBranch = branch; // fallback
893
- try {
894
- const { stdout } = await execAsync(`git rev-parse --abbrev-ref ${branch}@{upstream}`, { cwd: projectPath });
895
- const tracking = stdout.trim();
896
- remoteName = tracking.split('/')[0]; // Extract remote name
897
- remoteBranch = tracking.split('/').slice(1).join('/'); // Extract branch name
898
- } catch (error) {
899
- // No upstream, use fallback
900
- console.log('No upstream configured, using origin/branch as fallback');
901
- }
902
-
903
- const { stdout } = await execAsync(`git push ${remoteName} ${remoteBranch}`, { cwd: projectPath });
904
-
905
- res.json({
906
- success: true,
907
- output: stdout || 'Push completed successfully',
908
- remoteName,
909
- remoteBranch
910
- });
911
- } catch (error) {
912
- console.error('Git push error:', error);
913
-
914
- // Enhanced error handling for common push scenarios
915
- let errorMessage = 'Push failed';
916
- let details = error.message;
917
-
918
- if (error.message.includes('rejected')) {
919
- errorMessage = 'Push rejected';
920
- details = 'The remote has newer commits. Pull first to merge changes before pushing.';
921
- } else if (error.message.includes('non-fast-forward')) {
922
- errorMessage = 'Non-fast-forward push';
923
- details = 'Your branch is behind the remote. Pull the latest changes first.';
924
- } else if (error.message.includes('Could not resolve hostname')) {
925
- errorMessage = 'Network error';
926
- details = 'Unable to connect to remote repository. Check your internet connection.';
927
- } else if (error.message.includes('fatal: \'origin\' does not appear to be a git repository')) {
928
- errorMessage = 'Remote not configured';
929
- details = 'No remote repository configured. Add a remote with: git remote add origin <url>';
930
- } else if (error.message.includes('Permission denied')) {
931
- errorMessage = 'Authentication failed';
932
- details = 'Permission denied. Check your credentials or SSH keys.';
933
- } else if (error.message.includes('no upstream branch')) {
934
- errorMessage = 'No upstream branch';
935
- details = 'No upstream branch configured. Use: git push --set-upstream origin <branch>';
936
- }
937
-
938
- res.status(500).json({
939
- error: errorMessage,
940
- details: details
941
- });
942
- }
943
- });
944
-
945
- // Publish branch to remote (set upstream and push)
946
- router.post('/publish', async (req, res) => {
947
- const { project, branch } = req.body;
948
-
949
- if (!project || !branch) {
950
- return res.status(400).json({ error: 'Project name and branch are required' });
951
- }
952
-
953
- try {
954
- const projectPath = await getActualProjectPath(project);
955
- await validateGitRepository(projectPath);
956
-
957
- // Get current branch to verify it matches the requested branch
958
- const { stdout: currentBranch } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
959
- const currentBranchName = currentBranch.trim();
960
-
961
- if (currentBranchName !== branch) {
962
- return res.status(400).json({
963
- error: `Branch mismatch. Current branch is ${currentBranchName}, but trying to publish ${branch}`
964
- });
965
- }
966
-
967
- // Check if remote exists
968
- let remoteName = 'origin';
969
- try {
970
- const { stdout } = await execAsync('git remote', { cwd: projectPath });
971
- const remotes = stdout.trim().split('\n').filter(r => r.trim());
972
- if (remotes.length === 0) {
973
- return res.status(400).json({
974
- error: 'No remote repository configured. Add a remote with: git remote add origin <url>'
975
- });
976
- }
977
- remoteName = remotes.includes('origin') ? 'origin' : remotes[0];
978
- } catch (error) {
979
- return res.status(400).json({
980
- error: 'No remote repository configured. Add a remote with: git remote add origin <url>'
981
- });
982
- }
983
-
984
- // Publish the branch (set upstream and push)
985
- const { stdout } = await execAsync(`git push --set-upstream ${remoteName} ${branch}`, { cwd: projectPath });
986
-
987
- res.json({
988
- success: true,
989
- output: stdout || 'Branch published successfully',
990
- remoteName,
991
- branch
992
- });
993
- } catch (error) {
994
- console.error('Git publish error:', error);
995
-
996
- // Enhanced error handling for common publish scenarios
997
- let errorMessage = 'Publish failed';
998
- let details = error.message;
999
-
1000
- if (error.message.includes('rejected')) {
1001
- errorMessage = 'Publish rejected';
1002
- details = 'The remote branch already exists and has different commits. Use push instead.';
1003
- } else if (error.message.includes('Could not resolve hostname')) {
1004
- errorMessage = 'Network error';
1005
- details = 'Unable to connect to remote repository. Check your internet connection.';
1006
- } else if (error.message.includes('Permission denied')) {
1007
- errorMessage = 'Authentication failed';
1008
- details = 'Permission denied. Check your credentials or SSH keys.';
1009
- } else if (error.message.includes('fatal:') && error.message.includes('does not appear to be a git repository')) {
1010
- errorMessage = 'Remote not configured';
1011
- details = 'Remote repository not properly configured. Check your remote URL.';
1012
- }
1013
-
1014
- res.status(500).json({
1015
- error: errorMessage,
1016
- details: details
1017
- });
1018
- }
1019
- });
1020
-
1021
- // Discard changes for a specific file
1022
- router.post('/discard', async (req, res) => {
1023
- const { project, file } = req.body;
1024
-
1025
- if (!project || !file) {
1026
- return res.status(400).json({ error: 'Project name and file path are required' });
1027
- }
1028
-
1029
- try {
1030
- const projectPath = await getActualProjectPath(project);
1031
- await validateGitRepository(projectPath);
1032
-
1033
- // Check file status to determine correct discard command
1034
- const { stdout: statusOutput } = await execAsync(`git status --porcelain "${file}"`, { cwd: projectPath });
1035
-
1036
- if (!statusOutput.trim()) {
1037
- return res.status(400).json({ error: 'No changes to discard for this file' });
1038
- }
1039
-
1040
- const status = statusOutput.substring(0, 2);
1041
-
1042
- if (status === '??') {
1043
- // Untracked file or directory - delete it
1044
- const filePath = path.join(projectPath, file);
1045
- const stats = await fs.stat(filePath);
1046
-
1047
- if (stats.isDirectory()) {
1048
- await fs.rm(filePath, { recursive: true, force: true });
1049
- } else {
1050
- await fs.unlink(filePath);
1051
- }
1052
- } else if (status.includes('M') || status.includes('D')) {
1053
- // Modified or deleted file - restore from HEAD
1054
- await execAsync(`git restore "${file}"`, { cwd: projectPath });
1055
- } else if (status.includes('A')) {
1056
- // Added file - unstage it
1057
- await execAsync(`git reset HEAD "${file}"`, { cwd: projectPath });
1058
- }
1059
-
1060
- res.json({ success: true, message: `Changes discarded for ${file}` });
1061
- } catch (error) {
1062
- console.error('Git discard error:', error);
1063
- res.status(500).json({ error: error.message });
1064
- }
1065
- });
1066
-
1067
- // Delete untracked file
1068
- router.post('/delete-untracked', async (req, res) => {
1069
- const { project, file } = req.body;
1070
-
1071
- if (!project || !file) {
1072
- return res.status(400).json({ error: 'Project name and file path are required' });
1073
- }
1074
-
1075
- try {
1076
- const projectPath = await getActualProjectPath(project);
1077
- await validateGitRepository(projectPath);
1078
-
1079
- // Check if file is actually untracked
1080
- const { stdout: statusOutput } = await execAsync(`git status --porcelain "${file}"`, { cwd: projectPath });
1081
-
1082
- if (!statusOutput.trim()) {
1083
- return res.status(400).json({ error: 'File is not untracked or does not exist' });
1084
- }
1085
-
1086
- const status = statusOutput.substring(0, 2);
1087
-
1088
- if (status !== '??') {
1089
- return res.status(400).json({ error: 'File is not untracked. Use discard for tracked files.' });
1090
- }
1091
-
1092
- // Delete the untracked file or directory
1093
- const filePath = path.join(projectPath, file);
1094
- const stats = await fs.stat(filePath);
1095
-
1096
- if (stats.isDirectory()) {
1097
- // Use rm with recursive option for directories
1098
- await fs.rm(filePath, { recursive: true, force: true });
1099
- res.json({ success: true, message: `Untracked directory ${file} deleted successfully` });
1100
- } else {
1101
- await fs.unlink(filePath);
1102
- res.json({ success: true, message: `Untracked file ${file} deleted successfully` });
1103
- }
1104
- } catch (error) {
1105
- console.error('Git delete untracked error:', error);
1106
- res.status(500).json({ error: error.message });
1107
- }
1108
- });
1109
-
1110
- export default router;