@siteboon/claude-code-ui 1.23.2 → 1.24.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.
- package/dist/assets/{index-C6ZomNnQ.js → index-Cyy9g2W2.js} +181 -181
- package/dist/assets/index-Dlc1jmTz.css +32 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/index.js +46 -1
- package/server/projects.js +684 -5
- package/server/routes/gemini.js +7 -1
- package/server/routes/git.js +127 -71
- package/server/routes/projects.js +4 -6
- package/server/routes/user.js +22 -5
- package/server/utils/gitConfig.js +15 -5
- package/dist/assets/index-BFyod1Qa.css +0 -32
package/server/routes/gemini.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import sessionManager from '../sessionManager.js';
|
|
3
3
|
import { sessionNamesDb } from '../database/db.js';
|
|
4
|
+
import { getGeminiCliSessionMessages } from '../projects.js';
|
|
4
5
|
|
|
5
6
|
const router = express.Router();
|
|
6
7
|
|
|
@@ -12,7 +13,12 @@ router.get('/sessions/:sessionId/messages', async (req, res) => {
|
|
|
12
13
|
return res.status(400).json({ success: false, error: 'Invalid session ID format' });
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
let messages = sessionManager.getSessionMessages(sessionId);
|
|
17
|
+
|
|
18
|
+
// Fallback to Gemini CLI sessions on disk
|
|
19
|
+
if (messages.length === 0) {
|
|
20
|
+
messages = await getGeminiCliSessionMessages(sessionId);
|
|
21
|
+
}
|
|
16
22
|
|
|
17
23
|
res.json({
|
|
18
24
|
success: true,
|
package/server/routes/git.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
-
import {
|
|
3
|
-
import { promisify } from 'util';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
4
3
|
import path from 'path';
|
|
5
4
|
import { promises as fs } from 'fs';
|
|
6
5
|
import { extractProjectDirectory } from '../projects.js';
|
|
@@ -8,7 +7,6 @@ import { queryClaudeSDK } from '../claude-sdk.js';
|
|
|
8
7
|
import { spawnCursor } from '../cursor-cli.js';
|
|
9
8
|
|
|
10
9
|
const router = express.Router();
|
|
11
|
-
const execAsync = promisify(exec);
|
|
12
10
|
|
|
13
11
|
function spawnAsync(command, args, options = {}) {
|
|
14
12
|
return new Promise((resolve, reject) => {
|
|
@@ -47,6 +45,36 @@ function spawnAsync(command, args, options = {}) {
|
|
|
47
45
|
});
|
|
48
46
|
}
|
|
49
47
|
|
|
48
|
+
// Input validation helpers (defense-in-depth)
|
|
49
|
+
function validateCommitRef(commit) {
|
|
50
|
+
// Allow hex hashes, HEAD, HEAD~N, HEAD^N, tag names, branch names
|
|
51
|
+
if (!/^[a-zA-Z0-9._~^{}@\/-]+$/.test(commit)) {
|
|
52
|
+
throw new Error('Invalid commit reference');
|
|
53
|
+
}
|
|
54
|
+
return commit;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function validateBranchName(branch) {
|
|
58
|
+
if (!/^[a-zA-Z0-9._\/-]+$/.test(branch)) {
|
|
59
|
+
throw new Error('Invalid branch name');
|
|
60
|
+
}
|
|
61
|
+
return branch;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function validateFilePath(file) {
|
|
65
|
+
if (!file || file.includes('\0')) {
|
|
66
|
+
throw new Error('Invalid file path');
|
|
67
|
+
}
|
|
68
|
+
return file;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function validateRemoteName(remote) {
|
|
72
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(remote)) {
|
|
73
|
+
throw new Error('Invalid remote name');
|
|
74
|
+
}
|
|
75
|
+
return remote;
|
|
76
|
+
}
|
|
77
|
+
|
|
50
78
|
// Helper function to get the actual project path from the encoded project name
|
|
51
79
|
async function getActualProjectPath(projectName) {
|
|
52
80
|
try {
|
|
@@ -98,14 +126,14 @@ async function validateGitRepository(projectPath) {
|
|
|
98
126
|
|
|
99
127
|
try {
|
|
100
128
|
// Allow any directory that is inside a work tree (repo root or nested folder).
|
|
101
|
-
const { stdout: insideWorkTreeOutput } = await
|
|
129
|
+
const { stdout: insideWorkTreeOutput } = await spawnAsync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: projectPath });
|
|
102
130
|
const isInsideWorkTree = insideWorkTreeOutput.trim() === 'true';
|
|
103
131
|
if (!isInsideWorkTree) {
|
|
104
132
|
throw new Error('Not inside a git work tree');
|
|
105
133
|
}
|
|
106
134
|
|
|
107
135
|
// Ensure git can resolve the repository root for this directory.
|
|
108
|
-
await
|
|
136
|
+
await spawnAsync('git', ['rev-parse', '--show-toplevel'], { cwd: projectPath });
|
|
109
137
|
} catch {
|
|
110
138
|
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.');
|
|
111
139
|
}
|
|
@@ -129,7 +157,7 @@ router.get('/status', async (req, res) => {
|
|
|
129
157
|
let branch = 'main';
|
|
130
158
|
let hasCommits = true;
|
|
131
159
|
try {
|
|
132
|
-
const { stdout: branchOutput } = await
|
|
160
|
+
const { stdout: branchOutput } = await spawnAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: projectPath });
|
|
133
161
|
branch = branchOutput.trim();
|
|
134
162
|
} catch (error) {
|
|
135
163
|
// No HEAD exists - repository has no commits yet
|
|
@@ -142,7 +170,7 @@ router.get('/status', async (req, res) => {
|
|
|
142
170
|
}
|
|
143
171
|
|
|
144
172
|
// Get git status
|
|
145
|
-
const { stdout: statusOutput } = await
|
|
173
|
+
const { stdout: statusOutput } = await spawnAsync('git', ['status', '--porcelain'], { cwd: projectPath });
|
|
146
174
|
|
|
147
175
|
const modified = [];
|
|
148
176
|
const added = [];
|
|
@@ -201,8 +229,11 @@ router.get('/diff', async (req, res) => {
|
|
|
201
229
|
// Validate git repository
|
|
202
230
|
await validateGitRepository(projectPath);
|
|
203
231
|
|
|
232
|
+
// Validate file path
|
|
233
|
+
validateFilePath(file);
|
|
234
|
+
|
|
204
235
|
// Check if file is untracked or deleted
|
|
205
|
-
const { stdout: statusOutput } = await
|
|
236
|
+
const { stdout: statusOutput } = await spawnAsync('git', ['status', '--porcelain', file], { cwd: projectPath });
|
|
206
237
|
const isUntracked = statusOutput.startsWith('??');
|
|
207
238
|
const isDeleted = statusOutput.trim().startsWith('D ') || statusOutput.trim().startsWith(' D');
|
|
208
239
|
|
|
@@ -223,21 +254,21 @@ router.get('/diff', async (req, res) => {
|
|
|
223
254
|
}
|
|
224
255
|
} else if (isDeleted) {
|
|
225
256
|
// For deleted files, show the entire file content from HEAD as deletions
|
|
226
|
-
const { stdout: fileContent } = await
|
|
257
|
+
const { stdout: fileContent } = await spawnAsync('git', ['show', `HEAD:${file}`], { cwd: projectPath });
|
|
227
258
|
const lines = fileContent.split('\n');
|
|
228
259
|
diff = `--- a/${file}\n+++ /dev/null\n@@ -1,${lines.length} +0,0 @@\n` +
|
|
229
260
|
lines.map(line => `-${line}`).join('\n');
|
|
230
261
|
} else {
|
|
231
262
|
// Get diff for tracked files
|
|
232
263
|
// First check for unstaged changes (working tree vs index)
|
|
233
|
-
const { stdout: unstagedDiff } = await
|
|
264
|
+
const { stdout: unstagedDiff } = await spawnAsync('git', ['diff', '--', file], { cwd: projectPath });
|
|
234
265
|
|
|
235
266
|
if (unstagedDiff) {
|
|
236
267
|
// Show unstaged changes if they exist
|
|
237
268
|
diff = stripDiffHeaders(unstagedDiff);
|
|
238
269
|
} else {
|
|
239
270
|
// If no unstaged changes, check for staged changes (index vs HEAD)
|
|
240
|
-
const { stdout: stagedDiff } = await
|
|
271
|
+
const { stdout: stagedDiff } = await spawnAsync('git', ['diff', '--cached', '--', file], { cwd: projectPath });
|
|
241
272
|
diff = stripDiffHeaders(stagedDiff) || '';
|
|
242
273
|
}
|
|
243
274
|
}
|
|
@@ -263,8 +294,11 @@ router.get('/file-with-diff', async (req, res) => {
|
|
|
263
294
|
// Validate git repository
|
|
264
295
|
await validateGitRepository(projectPath);
|
|
265
296
|
|
|
297
|
+
// Validate file path
|
|
298
|
+
validateFilePath(file);
|
|
299
|
+
|
|
266
300
|
// Check file status
|
|
267
|
-
const { stdout: statusOutput } = await
|
|
301
|
+
const { stdout: statusOutput } = await spawnAsync('git', ['status', '--porcelain', file], { cwd: projectPath });
|
|
268
302
|
const isUntracked = statusOutput.startsWith('??');
|
|
269
303
|
const isDeleted = statusOutput.trim().startsWith('D ') || statusOutput.trim().startsWith(' D');
|
|
270
304
|
|
|
@@ -273,7 +307,7 @@ router.get('/file-with-diff', async (req, res) => {
|
|
|
273
307
|
|
|
274
308
|
if (isDeleted) {
|
|
275
309
|
// For deleted files, get content from HEAD
|
|
276
|
-
const { stdout: headContent } = await
|
|
310
|
+
const { stdout: headContent } = await spawnAsync('git', ['show', `HEAD:${file}`], { cwd: projectPath });
|
|
277
311
|
oldContent = headContent;
|
|
278
312
|
currentContent = headContent; // Show the deleted content in editor
|
|
279
313
|
} else {
|
|
@@ -291,7 +325,7 @@ router.get('/file-with-diff', async (req, res) => {
|
|
|
291
325
|
if (!isUntracked) {
|
|
292
326
|
// Get the old content from HEAD for tracked files
|
|
293
327
|
try {
|
|
294
|
-
const { stdout: headContent } = await
|
|
328
|
+
const { stdout: headContent } = await spawnAsync('git', ['show', `HEAD:${file}`], { cwd: projectPath });
|
|
295
329
|
oldContent = headContent;
|
|
296
330
|
} catch (error) {
|
|
297
331
|
// File might be newly added to git (staged but not committed)
|
|
@@ -328,17 +362,17 @@ router.post('/initial-commit', async (req, res) => {
|
|
|
328
362
|
|
|
329
363
|
// Check if there are already commits
|
|
330
364
|
try {
|
|
331
|
-
await
|
|
365
|
+
await spawnAsync('git', ['rev-parse', 'HEAD'], { cwd: projectPath });
|
|
332
366
|
return res.status(400).json({ error: 'Repository already has commits. Use regular commit instead.' });
|
|
333
367
|
} catch (error) {
|
|
334
368
|
// No HEAD - this is good, we can create initial commit
|
|
335
369
|
}
|
|
336
370
|
|
|
337
371
|
// Add all files
|
|
338
|
-
await
|
|
372
|
+
await spawnAsync('git', ['add', '.'], { cwd: projectPath });
|
|
339
373
|
|
|
340
374
|
// Create initial commit
|
|
341
|
-
const { stdout } = await
|
|
375
|
+
const { stdout } = await spawnAsync('git', ['commit', '-m', 'Initial commit'], { cwd: projectPath });
|
|
342
376
|
|
|
343
377
|
res.json({ success: true, output: stdout, message: 'Initial commit created successfully' });
|
|
344
378
|
} catch (error) {
|
|
@@ -372,11 +406,12 @@ router.post('/commit', async (req, res) => {
|
|
|
372
406
|
|
|
373
407
|
// Stage selected files
|
|
374
408
|
for (const file of files) {
|
|
375
|
-
|
|
409
|
+
validateFilePath(file);
|
|
410
|
+
await spawnAsync('git', ['add', file], { cwd: projectPath });
|
|
376
411
|
}
|
|
377
|
-
|
|
412
|
+
|
|
378
413
|
// Commit with message
|
|
379
|
-
const { stdout } = await
|
|
414
|
+
const { stdout } = await spawnAsync('git', ['commit', '-m', message], { cwd: projectPath });
|
|
380
415
|
|
|
381
416
|
res.json({ success: true, output: stdout });
|
|
382
417
|
} catch (error) {
|
|
@@ -400,7 +435,7 @@ router.get('/branches', async (req, res) => {
|
|
|
400
435
|
await validateGitRepository(projectPath);
|
|
401
436
|
|
|
402
437
|
// Get all branches
|
|
403
|
-
const { stdout } = await
|
|
438
|
+
const { stdout } = await spawnAsync('git', ['branch', '-a'], { cwd: projectPath });
|
|
404
439
|
|
|
405
440
|
// Parse branches
|
|
406
441
|
const branches = stdout
|
|
@@ -439,7 +474,8 @@ router.post('/checkout', async (req, res) => {
|
|
|
439
474
|
const projectPath = await getActualProjectPath(project);
|
|
440
475
|
|
|
441
476
|
// Checkout the branch
|
|
442
|
-
|
|
477
|
+
validateBranchName(branch);
|
|
478
|
+
const { stdout } = await spawnAsync('git', ['checkout', branch], { cwd: projectPath });
|
|
443
479
|
|
|
444
480
|
res.json({ success: true, output: stdout });
|
|
445
481
|
} catch (error) {
|
|
@@ -460,7 +496,8 @@ router.post('/create-branch', async (req, res) => {
|
|
|
460
496
|
const projectPath = await getActualProjectPath(project);
|
|
461
497
|
|
|
462
498
|
// Create and checkout new branch
|
|
463
|
-
|
|
499
|
+
validateBranchName(branch);
|
|
500
|
+
const { stdout } = await spawnAsync('git', ['checkout', '-b', branch], { cwd: projectPath });
|
|
464
501
|
|
|
465
502
|
res.json({ success: true, output: stdout });
|
|
466
503
|
} catch (error) {
|
|
@@ -509,8 +546,8 @@ router.get('/commits', async (req, res) => {
|
|
|
509
546
|
// Get stats for each commit
|
|
510
547
|
for (const commit of commits) {
|
|
511
548
|
try {
|
|
512
|
-
const { stdout: stats } = await
|
|
513
|
-
|
|
549
|
+
const { stdout: stats } = await spawnAsync(
|
|
550
|
+
'git', ['show', '--stat', '--format=', commit.hash],
|
|
514
551
|
{ cwd: projectPath }
|
|
515
552
|
);
|
|
516
553
|
commit.stats = stats.trim().split('\n').pop(); // Get the summary line
|
|
@@ -536,10 +573,13 @@ router.get('/commit-diff', async (req, res) => {
|
|
|
536
573
|
|
|
537
574
|
try {
|
|
538
575
|
const projectPath = await getActualProjectPath(project);
|
|
539
|
-
|
|
576
|
+
|
|
577
|
+
// Validate commit reference (defense-in-depth)
|
|
578
|
+
validateCommitRef(commit);
|
|
579
|
+
|
|
540
580
|
// Get diff for the commit
|
|
541
|
-
const { stdout } = await
|
|
542
|
-
|
|
581
|
+
const { stdout } = await spawnAsync(
|
|
582
|
+
'git', ['show', commit],
|
|
543
583
|
{ cwd: projectPath }
|
|
544
584
|
);
|
|
545
585
|
|
|
@@ -570,8 +610,9 @@ router.post('/generate-commit-message', async (req, res) => {
|
|
|
570
610
|
let diffContext = '';
|
|
571
611
|
for (const file of files) {
|
|
572
612
|
try {
|
|
573
|
-
|
|
574
|
-
|
|
613
|
+
validateFilePath(file);
|
|
614
|
+
const { stdout } = await spawnAsync(
|
|
615
|
+
'git', ['diff', 'HEAD', '--', file],
|
|
575
616
|
{ cwd: projectPath }
|
|
576
617
|
);
|
|
577
618
|
if (stdout) {
|
|
@@ -764,14 +805,14 @@ router.get('/remote-status', async (req, res) => {
|
|
|
764
805
|
await validateGitRepository(projectPath);
|
|
765
806
|
|
|
766
807
|
// Get current branch
|
|
767
|
-
const { stdout: currentBranch } = await
|
|
808
|
+
const { stdout: currentBranch } = await spawnAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: projectPath });
|
|
768
809
|
const branch = currentBranch.trim();
|
|
769
810
|
|
|
770
811
|
// Check if there's a remote tracking branch (smart detection)
|
|
771
812
|
let trackingBranch;
|
|
772
813
|
let remoteName;
|
|
773
814
|
try {
|
|
774
|
-
const { stdout } = await
|
|
815
|
+
const { stdout } = await spawnAsync('git', ['rev-parse', '--abbrev-ref', `${branch}@{upstream}`], { cwd: projectPath });
|
|
775
816
|
trackingBranch = stdout.trim();
|
|
776
817
|
remoteName = trackingBranch.split('/')[0]; // Extract remote name (e.g., "origin/main" -> "origin")
|
|
777
818
|
} catch (error) {
|
|
@@ -779,7 +820,7 @@ router.get('/remote-status', async (req, res) => {
|
|
|
779
820
|
let hasRemote = false;
|
|
780
821
|
let remoteName = null;
|
|
781
822
|
try {
|
|
782
|
-
const { stdout } = await
|
|
823
|
+
const { stdout } = await spawnAsync('git', ['remote'], { cwd: projectPath });
|
|
783
824
|
const remotes = stdout.trim().split('\n').filter(r => r.trim());
|
|
784
825
|
if (remotes.length > 0) {
|
|
785
826
|
hasRemote = true;
|
|
@@ -788,8 +829,8 @@ router.get('/remote-status', async (req, res) => {
|
|
|
788
829
|
} catch (remoteError) {
|
|
789
830
|
// No remotes configured
|
|
790
831
|
}
|
|
791
|
-
|
|
792
|
-
return res.json({
|
|
832
|
+
|
|
833
|
+
return res.json({
|
|
793
834
|
hasRemote,
|
|
794
835
|
hasUpstream: false,
|
|
795
836
|
branch,
|
|
@@ -799,8 +840,8 @@ router.get('/remote-status', async (req, res) => {
|
|
|
799
840
|
}
|
|
800
841
|
|
|
801
842
|
// Get ahead/behind counts
|
|
802
|
-
const { stdout: countOutput } = await
|
|
803
|
-
|
|
843
|
+
const { stdout: countOutput } = await spawnAsync(
|
|
844
|
+
'git', ['rev-list', '--count', '--left-right', `${trackingBranch}...HEAD`],
|
|
804
845
|
{ cwd: projectPath }
|
|
805
846
|
);
|
|
806
847
|
|
|
@@ -835,20 +876,21 @@ router.post('/fetch', async (req, res) => {
|
|
|
835
876
|
await validateGitRepository(projectPath);
|
|
836
877
|
|
|
837
878
|
// Get current branch and its upstream remote
|
|
838
|
-
const { stdout: currentBranch } = await
|
|
879
|
+
const { stdout: currentBranch } = await spawnAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: projectPath });
|
|
839
880
|
const branch = currentBranch.trim();
|
|
840
881
|
|
|
841
882
|
let remoteName = 'origin'; // fallback
|
|
842
883
|
try {
|
|
843
|
-
const { stdout } = await
|
|
884
|
+
const { stdout } = await spawnAsync('git', ['rev-parse', '--abbrev-ref', `${branch}@{upstream}`], { cwd: projectPath });
|
|
844
885
|
remoteName = stdout.trim().split('/')[0]; // Extract remote name
|
|
845
886
|
} catch (error) {
|
|
846
887
|
// No upstream, try to fetch from origin anyway
|
|
847
888
|
console.log('No upstream configured, using origin as fallback');
|
|
848
889
|
}
|
|
849
890
|
|
|
850
|
-
|
|
851
|
-
|
|
891
|
+
validateRemoteName(remoteName);
|
|
892
|
+
const { stdout } = await spawnAsync('git', ['fetch', remoteName], { cwd: projectPath });
|
|
893
|
+
|
|
852
894
|
res.json({ success: true, output: stdout || 'Fetch completed successfully', remoteName });
|
|
853
895
|
} catch (error) {
|
|
854
896
|
console.error('Git fetch error:', error);
|
|
@@ -876,13 +918,13 @@ router.post('/pull', async (req, res) => {
|
|
|
876
918
|
await validateGitRepository(projectPath);
|
|
877
919
|
|
|
878
920
|
// Get current branch and its upstream remote
|
|
879
|
-
const { stdout: currentBranch } = await
|
|
921
|
+
const { stdout: currentBranch } = await spawnAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: projectPath });
|
|
880
922
|
const branch = currentBranch.trim();
|
|
881
923
|
|
|
882
924
|
let remoteName = 'origin'; // fallback
|
|
883
925
|
let remoteBranch = branch; // fallback
|
|
884
926
|
try {
|
|
885
|
-
const { stdout } = await
|
|
927
|
+
const { stdout } = await spawnAsync('git', ['rev-parse', '--abbrev-ref', `${branch}@{upstream}`], { cwd: projectPath });
|
|
886
928
|
const tracking = stdout.trim();
|
|
887
929
|
remoteName = tracking.split('/')[0]; // Extract remote name
|
|
888
930
|
remoteBranch = tracking.split('/').slice(1).join('/'); // Extract branch name
|
|
@@ -891,17 +933,19 @@ router.post('/pull', async (req, res) => {
|
|
|
891
933
|
console.log('No upstream configured, using origin/branch as fallback');
|
|
892
934
|
}
|
|
893
935
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
936
|
+
validateRemoteName(remoteName);
|
|
937
|
+
validateBranchName(remoteBranch);
|
|
938
|
+
const { stdout } = await spawnAsync('git', ['pull', remoteName, remoteBranch], { cwd: projectPath });
|
|
939
|
+
|
|
940
|
+
res.json({
|
|
941
|
+
success: true,
|
|
942
|
+
output: stdout || 'Pull completed successfully',
|
|
899
943
|
remoteName,
|
|
900
944
|
remoteBranch
|
|
901
945
|
});
|
|
902
946
|
} catch (error) {
|
|
903
947
|
console.error('Git pull error:', error);
|
|
904
|
-
|
|
948
|
+
|
|
905
949
|
// Enhanced error handling for common pull scenarios
|
|
906
950
|
let errorMessage = 'Pull failed';
|
|
907
951
|
let details = error.message;
|
|
@@ -943,13 +987,13 @@ router.post('/push', async (req, res) => {
|
|
|
943
987
|
await validateGitRepository(projectPath);
|
|
944
988
|
|
|
945
989
|
// Get current branch and its upstream remote
|
|
946
|
-
const { stdout: currentBranch } = await
|
|
990
|
+
const { stdout: currentBranch } = await spawnAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: projectPath });
|
|
947
991
|
const branch = currentBranch.trim();
|
|
948
992
|
|
|
949
993
|
let remoteName = 'origin'; // fallback
|
|
950
994
|
let remoteBranch = branch; // fallback
|
|
951
995
|
try {
|
|
952
|
-
const { stdout } = await
|
|
996
|
+
const { stdout } = await spawnAsync('git', ['rev-parse', '--abbrev-ref', `${branch}@{upstream}`], { cwd: projectPath });
|
|
953
997
|
const tracking = stdout.trim();
|
|
954
998
|
remoteName = tracking.split('/')[0]; // Extract remote name
|
|
955
999
|
remoteBranch = tracking.split('/').slice(1).join('/'); // Extract branch name
|
|
@@ -958,11 +1002,13 @@ router.post('/push', async (req, res) => {
|
|
|
958
1002
|
console.log('No upstream configured, using origin/branch as fallback');
|
|
959
1003
|
}
|
|
960
1004
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1005
|
+
validateRemoteName(remoteName);
|
|
1006
|
+
validateBranchName(remoteBranch);
|
|
1007
|
+
const { stdout } = await spawnAsync('git', ['push', remoteName, remoteBranch], { cwd: projectPath });
|
|
1008
|
+
|
|
1009
|
+
res.json({
|
|
1010
|
+
success: true,
|
|
1011
|
+
output: stdout || 'Push completed successfully',
|
|
966
1012
|
remoteName,
|
|
967
1013
|
remoteBranch
|
|
968
1014
|
});
|
|
@@ -1012,35 +1058,39 @@ router.post('/publish', async (req, res) => {
|
|
|
1012
1058
|
const projectPath = await getActualProjectPath(project);
|
|
1013
1059
|
await validateGitRepository(projectPath);
|
|
1014
1060
|
|
|
1061
|
+
// Validate branch name
|
|
1062
|
+
validateBranchName(branch);
|
|
1063
|
+
|
|
1015
1064
|
// Get current branch to verify it matches the requested branch
|
|
1016
|
-
const { stdout: currentBranch } = await
|
|
1065
|
+
const { stdout: currentBranch } = await spawnAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: projectPath });
|
|
1017
1066
|
const currentBranchName = currentBranch.trim();
|
|
1018
|
-
|
|
1067
|
+
|
|
1019
1068
|
if (currentBranchName !== branch) {
|
|
1020
|
-
return res.status(400).json({
|
|
1021
|
-
error: `Branch mismatch. Current branch is ${currentBranchName}, but trying to publish ${branch}`
|
|
1069
|
+
return res.status(400).json({
|
|
1070
|
+
error: `Branch mismatch. Current branch is ${currentBranchName}, but trying to publish ${branch}`
|
|
1022
1071
|
});
|
|
1023
1072
|
}
|
|
1024
1073
|
|
|
1025
1074
|
// Check if remote exists
|
|
1026
1075
|
let remoteName = 'origin';
|
|
1027
1076
|
try {
|
|
1028
|
-
const { stdout } = await
|
|
1077
|
+
const { stdout } = await spawnAsync('git', ['remote'], { cwd: projectPath });
|
|
1029
1078
|
const remotes = stdout.trim().split('\n').filter(r => r.trim());
|
|
1030
1079
|
if (remotes.length === 0) {
|
|
1031
|
-
return res.status(400).json({
|
|
1032
|
-
error: 'No remote repository configured. Add a remote with: git remote add origin <url>'
|
|
1080
|
+
return res.status(400).json({
|
|
1081
|
+
error: 'No remote repository configured. Add a remote with: git remote add origin <url>'
|
|
1033
1082
|
});
|
|
1034
1083
|
}
|
|
1035
1084
|
remoteName = remotes.includes('origin') ? 'origin' : remotes[0];
|
|
1036
1085
|
} catch (error) {
|
|
1037
|
-
return res.status(400).json({
|
|
1038
|
-
error: 'No remote repository configured. Add a remote with: git remote add origin <url>'
|
|
1086
|
+
return res.status(400).json({
|
|
1087
|
+
error: 'No remote repository configured. Add a remote with: git remote add origin <url>'
|
|
1039
1088
|
});
|
|
1040
1089
|
}
|
|
1041
1090
|
|
|
1042
1091
|
// Publish the branch (set upstream and push)
|
|
1043
|
-
|
|
1092
|
+
validateRemoteName(remoteName);
|
|
1093
|
+
const { stdout } = await spawnAsync('git', ['push', '--set-upstream', remoteName, branch], { cwd: projectPath });
|
|
1044
1094
|
|
|
1045
1095
|
res.json({
|
|
1046
1096
|
success: true,
|
|
@@ -1088,9 +1138,12 @@ router.post('/discard', async (req, res) => {
|
|
|
1088
1138
|
const projectPath = await getActualProjectPath(project);
|
|
1089
1139
|
await validateGitRepository(projectPath);
|
|
1090
1140
|
|
|
1141
|
+
// Validate file path
|
|
1142
|
+
validateFilePath(file);
|
|
1143
|
+
|
|
1091
1144
|
// Check file status to determine correct discard command
|
|
1092
|
-
const { stdout: statusOutput } = await
|
|
1093
|
-
|
|
1145
|
+
const { stdout: statusOutput } = await spawnAsync('git', ['status', '--porcelain', file], { cwd: projectPath });
|
|
1146
|
+
|
|
1094
1147
|
if (!statusOutput.trim()) {
|
|
1095
1148
|
return res.status(400).json({ error: 'No changes to discard for this file' });
|
|
1096
1149
|
}
|
|
@@ -1109,10 +1162,10 @@ router.post('/discard', async (req, res) => {
|
|
|
1109
1162
|
}
|
|
1110
1163
|
} else if (status.includes('M') || status.includes('D')) {
|
|
1111
1164
|
// Modified or deleted file - restore from HEAD
|
|
1112
|
-
await
|
|
1165
|
+
await spawnAsync('git', ['restore', file], { cwd: projectPath });
|
|
1113
1166
|
} else if (status.includes('A')) {
|
|
1114
1167
|
// Added file - unstage it
|
|
1115
|
-
await
|
|
1168
|
+
await spawnAsync('git', ['reset', 'HEAD', file], { cwd: projectPath });
|
|
1116
1169
|
}
|
|
1117
1170
|
|
|
1118
1171
|
res.json({ success: true, message: `Changes discarded for ${file}` });
|
|
@@ -1134,8 +1187,11 @@ router.post('/delete-untracked', async (req, res) => {
|
|
|
1134
1187
|
const projectPath = await getActualProjectPath(project);
|
|
1135
1188
|
await validateGitRepository(projectPath);
|
|
1136
1189
|
|
|
1190
|
+
// Validate file path
|
|
1191
|
+
validateFilePath(file);
|
|
1192
|
+
|
|
1137
1193
|
// Check if file is actually untracked
|
|
1138
|
-
const { stdout: statusOutput } = await
|
|
1194
|
+
const { stdout: statusOutput } = await spawnAsync('git', ['status', '--porcelain', file], { cwd: projectPath });
|
|
1139
1195
|
|
|
1140
1196
|
if (!statusOutput.trim()) {
|
|
1141
1197
|
return res.status(400).json({ error: 'File is not untracked or does not exist' });
|
|
@@ -311,13 +311,11 @@ router.post('/create-workspace', async (req, res) => {
|
|
|
311
311
|
* Helper function to get GitHub token from database
|
|
312
312
|
*/
|
|
313
313
|
async function getGithubTokenById(tokenId, userId) {
|
|
314
|
-
const {
|
|
315
|
-
const db = await getDatabase();
|
|
314
|
+
const { db } = await import('../database/db.js');
|
|
316
315
|
|
|
317
|
-
const credential =
|
|
318
|
-
'SELECT * FROM user_credentials WHERE id = ? AND user_id = ? AND credential_type = ? AND is_active = 1'
|
|
319
|
-
|
|
320
|
-
);
|
|
316
|
+
const credential = db.prepare(
|
|
317
|
+
'SELECT * FROM user_credentials WHERE id = ? AND user_id = ? AND credential_type = ? AND is_active = 1'
|
|
318
|
+
).get(tokenId, userId, 'github_token');
|
|
321
319
|
|
|
322
320
|
// Return in the expected format (github_token field for compatibility)
|
|
323
321
|
if (credential) {
|
package/server/routes/user.js
CHANGED
|
@@ -2,12 +2,29 @@ import express from 'express';
|
|
|
2
2
|
import { userDb } from '../database/db.js';
|
|
3
3
|
import { authenticateToken } from '../middleware/auth.js';
|
|
4
4
|
import { getSystemGitConfig } from '../utils/gitConfig.js';
|
|
5
|
-
import {
|
|
6
|
-
import { promisify } from 'util';
|
|
5
|
+
import { spawn } from 'child_process';
|
|
7
6
|
|
|
8
|
-
const execAsync = promisify(exec);
|
|
9
7
|
const router = express.Router();
|
|
10
8
|
|
|
9
|
+
function spawnAsync(command, args, options = {}) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const child = spawn(command, args, { ...options, shell: false });
|
|
12
|
+
let stdout = '';
|
|
13
|
+
let stderr = '';
|
|
14
|
+
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
15
|
+
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
16
|
+
child.on('error', (error) => { reject(error); });
|
|
17
|
+
child.on('close', (code) => {
|
|
18
|
+
if (code === 0) { resolve({ stdout, stderr }); return; }
|
|
19
|
+
const error = new Error(`Command failed: ${command} ${args.join(' ')}`);
|
|
20
|
+
error.code = code;
|
|
21
|
+
error.stdout = stdout;
|
|
22
|
+
error.stderr = stderr;
|
|
23
|
+
reject(error);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
11
28
|
router.get('/git-config', authenticateToken, async (req, res) => {
|
|
12
29
|
try {
|
|
13
30
|
const userId = req.user.id;
|
|
@@ -55,8 +72,8 @@ router.post('/git-config', authenticateToken, async (req, res) => {
|
|
|
55
72
|
userDb.updateGitConfig(userId, gitName, gitEmail);
|
|
56
73
|
|
|
57
74
|
try {
|
|
58
|
-
await
|
|
59
|
-
await
|
|
75
|
+
await spawnAsync('git', ['config', '--global', 'user.name', gitName]);
|
|
76
|
+
await spawnAsync('git', ['config', '--global', 'user.email', gitEmail]);
|
|
60
77
|
console.log(`Applied git config globally: ${gitName} <${gitEmail}>`);
|
|
61
78
|
} catch (gitError) {
|
|
62
79
|
console.error('Error applying git config:', gitError);
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { promisify } from 'util';
|
|
1
|
+
import { spawn } from 'child_process';
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
function spawnAsync(command, args) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const child = spawn(command, args, { shell: false });
|
|
6
|
+
let stdout = '';
|
|
7
|
+
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
8
|
+
child.on('error', (error) => { reject(error); });
|
|
9
|
+
child.on('close', (code) => {
|
|
10
|
+
if (code === 0) { resolve({ stdout }); return; }
|
|
11
|
+
reject(new Error(`Command failed with code ${code}`));
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
}
|
|
5
15
|
|
|
6
16
|
/**
|
|
7
17
|
* Read git configuration from system's global git config
|
|
@@ -10,8 +20,8 @@ const execAsync = promisify(exec);
|
|
|
10
20
|
export async function getSystemGitConfig() {
|
|
11
21
|
try {
|
|
12
22
|
const [nameResult, emailResult] = await Promise.all([
|
|
13
|
-
|
|
14
|
-
|
|
23
|
+
spawnAsync('git', ['config', '--global', 'user.name']).catch(() => ({ stdout: '' })),
|
|
24
|
+
spawnAsync('git', ['config', '--global', 'user.email']).catch(() => ({ stdout: '' }))
|
|
15
25
|
]);
|
|
16
26
|
|
|
17
27
|
return {
|