@siteboon/claude-code-ui 1.23.2 → 1.25.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.
@@ -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
- const messages = sessionManager.getSessionMessages(sessionId);
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,
@@ -1,6 +1,5 @@
1
1
  import express from 'express';
2
- import { exec, spawn } from 'child_process';
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 execAsync('git rev-parse --is-inside-work-tree', { cwd: projectPath });
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 execAsync('git rev-parse --show-toplevel', { cwd: projectPath });
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 execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
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 execAsync('git status --porcelain', { cwd: projectPath });
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 execAsync(`git status --porcelain "${file}"`, { cwd: projectPath });
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 execAsync(`git show HEAD:"${file}"`, { cwd: projectPath });
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 execAsync(`git diff -- "${file}"`, { cwd: projectPath });
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 execAsync(`git diff --cached -- "${file}"`, { cwd: projectPath });
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 execAsync(`git status --porcelain "${file}"`, { cwd: projectPath });
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 execAsync(`git show HEAD:"${file}"`, { cwd: projectPath });
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 execAsync(`git show HEAD:"${file}"`, { cwd: projectPath });
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 execAsync('git rev-parse HEAD', { cwd: projectPath });
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 execAsync('git add .', { cwd: projectPath });
372
+ await spawnAsync('git', ['add', '.'], { cwd: projectPath });
339
373
 
340
374
  // Create initial commit
341
- const { stdout } = await execAsync('git commit -m "Initial commit"', { cwd: projectPath });
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
- await execAsync(`git add "${file}"`, { cwd: projectPath });
409
+ validateFilePath(file);
410
+ await spawnAsync('git', ['add', file], { cwd: projectPath });
376
411
  }
377
-
412
+
378
413
  // Commit with message
379
- const { stdout } = await execAsync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { cwd: projectPath });
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 execAsync('git branch -a', { cwd: projectPath });
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
- const { stdout } = await execAsync(`git checkout "${branch}"`, { cwd: projectPath });
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
- const { stdout } = await execAsync(`git checkout -b "${branch}"`, { cwd: projectPath });
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 execAsync(
513
- `git show --stat --format='' ${commit.hash}`,
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 execAsync(
542
- `git show ${commit}`,
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
- const { stdout } = await execAsync(
574
- `git diff HEAD -- "${file}"`,
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 execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
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 execAsync(`git rev-parse --abbrev-ref ${branch}@{upstream}`, { cwd: projectPath });
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 execAsync('git remote', { cwd: projectPath });
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 execAsync(
803
- `git rev-list --count --left-right ${trackingBranch}...HEAD`,
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 execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
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 execAsync(`git rev-parse --abbrev-ref ${branch}@{upstream}`, { cwd: projectPath });
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
- const { stdout } = await execAsync(`git fetch ${remoteName}`, { cwd: projectPath });
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 execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
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 execAsync(`git rev-parse --abbrev-ref ${branch}@{upstream}`, { cwd: projectPath });
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
- const { stdout } = await execAsync(`git pull ${remoteName} ${remoteBranch}`, { cwd: projectPath });
895
-
896
- res.json({
897
- success: true,
898
- output: stdout || 'Pull completed successfully',
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 execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
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 execAsync(`git rev-parse --abbrev-ref ${branch}@{upstream}`, { cwd: projectPath });
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
- const { stdout } = await execAsync(`git push ${remoteName} ${remoteBranch}`, { cwd: projectPath });
962
-
963
- res.json({
964
- success: true,
965
- output: stdout || 'Push completed successfully',
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 execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
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 execAsync('git remote', { cwd: projectPath });
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
- const { stdout } = await execAsync(`git push --set-upstream ${remoteName} ${branch}`, { cwd: projectPath });
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 execAsync(`git status --porcelain "${file}"`, { cwd: projectPath });
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 execAsync(`git restore "${file}"`, { cwd: projectPath });
1165
+ await spawnAsync('git', ['restore', file], { cwd: projectPath });
1113
1166
  } else if (status.includes('A')) {
1114
1167
  // Added file - unstage it
1115
- await execAsync(`git reset HEAD "${file}"`, { cwd: projectPath });
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 execAsync(`git status --porcelain "${file}"`, { cwd: projectPath });
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' });