phantomx-tool-client 1.0.8 → 1.0.9

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.
@@ -40,6 +40,8 @@ exports.getRepoList = getRepoList;
40
40
  exports.getRepoBranch = getRepoBranch;
41
41
  const Logger_1 = require("./Services/Logger");
42
42
  const child_process_1 = require("child_process");
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
43
45
  // ---------------------------------------------------------------------------
44
46
  // Local bash executor — drop-in replacement for ssh.executeCommand()
45
47
  // Returns { success, output, error, code } matching the SSH client response.
@@ -313,6 +315,246 @@ async function getSingleFileDiff(repoPath, filePath, status) {
313
315
  }
314
316
  }
315
317
  class GithubOperationsService {
318
+ constructor(localWorkspacePath) {
319
+ this.localWorkspacePath = localWorkspacePath;
320
+ }
321
+ resolveWorkspacePath(targetPath) {
322
+ const workspace = path.resolve(this.localWorkspacePath || tool_server_1.folderPath);
323
+ const resolved = targetPath
324
+ ? path.resolve(path.isAbsolute(targetPath) ? targetPath : path.join(workspace, targetPath))
325
+ : workspace;
326
+ const relative = path.relative(workspace, resolved);
327
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
328
+ throw new Error(`Path is outside the workspace: ${targetPath}`);
329
+ }
330
+ return resolved;
331
+ }
332
+ resolveLocalRepository(repoPathOrName) {
333
+ const candidate = path.isAbsolute(repoPathOrName)
334
+ ? repoPathOrName
335
+ : path.join(this.localWorkspacePath || tool_server_1.folderPath, repoPathOrName);
336
+ const repoPath = this.resolveWorkspacePath(candidate);
337
+ if (!fs.existsSync(repoPath)) {
338
+ throw new Error(`Repository path does not exist: ${repoPath}`);
339
+ }
340
+ if (!fs.existsSync(path.join(repoPath, '.git'))) {
341
+ throw new Error(`Not a git repository: ${repoPath}`);
342
+ }
343
+ return repoPath;
344
+ }
345
+ async findLocalGitRepositories(rootPath) {
346
+ const repositories = [];
347
+ const search = async (currentPath, depth) => {
348
+ if (depth > 10)
349
+ return;
350
+ if (fs.existsSync(path.join(currentPath, '.git'))) {
351
+ repositories.push(currentPath);
352
+ return;
353
+ }
354
+ let entries;
355
+ try {
356
+ entries = await fs.promises.readdir(currentPath, { withFileTypes: true });
357
+ }
358
+ catch {
359
+ logger.warn(`Cannot read directory: ${currentPath}`);
360
+ return;
361
+ }
362
+ for (const entry of entries) {
363
+ if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules')
364
+ continue;
365
+ await search(path.join(currentPath, entry.name), depth + 1);
366
+ }
367
+ };
368
+ await search(rootPath, 0);
369
+ return repositories;
370
+ }
371
+ getDiffStats(fileStats) {
372
+ return {
373
+ insertions: Number(fileStats === null || fileStats === void 0 ? void 0 : fileStats.insertions) || 0,
374
+ deletions: Number(fileStats === null || fileStats === void 0 ? void 0 : fileStats.deletions) || 0
375
+ };
376
+ }
377
+ async getLocalRepositoryChanges(repoPath) {
378
+ const git = (0, simple_git_1.simpleGit)(repoPath);
379
+ const status = await git.status();
380
+ const stagedSummary = await git.diffSummary(['--cached']);
381
+ const unstagedSummary = await git.diffSummary();
382
+ const changes = [];
383
+ const processed = new Set();
384
+ const addChange = (file, statusCode, insertions, deletions) => {
385
+ changes.push({ file, status: statusCode, insertions, deletions, changes: insertions + deletions });
386
+ processed.add(file);
387
+ };
388
+ for (const file of status.modified) {
389
+ const isStaged = status.staged.includes(file);
390
+ const unstaged = this.getDiffStats(unstagedSummary.files.find(item => item.file === file));
391
+ const staged = isStaged
392
+ ? this.getDiffStats(stagedSummary.files.find(item => item.file === file))
393
+ : { insertions: 0, deletions: 0 };
394
+ addChange(file, isStaged ? 'MM' : 'M', unstaged.insertions + staged.insertions, unstaged.deletions + staged.deletions);
395
+ }
396
+ for (const file of status.staged) {
397
+ if (processed.has(file) || status.deleted.includes(file))
398
+ continue;
399
+ const stats = this.getDiffStats(stagedSummary.files.find(item => item.file === file));
400
+ addChange(file, 'A', stats.insertions, stats.deletions);
401
+ }
402
+ for (const file of status.not_added) {
403
+ if (processed.has(file))
404
+ continue;
405
+ try {
406
+ const stat = await fs.promises.stat(path.join(repoPath, file));
407
+ addChange(file, '?', Math.max(1, Math.ceil(stat.size / 50)), 0);
408
+ }
409
+ catch {
410
+ addChange(file, '?', 1, 0);
411
+ }
412
+ }
413
+ for (const file of status.created || []) {
414
+ if (processed.has(file))
415
+ continue;
416
+ try {
417
+ const stat = await fs.promises.stat(path.join(repoPath, file));
418
+ addChange(file, '?', Math.max(1, Math.ceil(stat.size / 50)), 0);
419
+ }
420
+ catch {
421
+ addChange(file, '?', 1, 0);
422
+ }
423
+ }
424
+ for (const file of status.deleted) {
425
+ if (processed.has(file))
426
+ continue;
427
+ const summary = status.staged.includes(file) ? stagedSummary : unstagedSummary;
428
+ const stats = this.getDiffStats(summary.files.find(item => item.file === file));
429
+ addChange(file, 'D', 0, stats.deletions || 1);
430
+ }
431
+ for (const rename of status.renamed) {
432
+ if (processed.has(rename.to))
433
+ continue;
434
+ const staged = this.getDiffStats(stagedSummary.files.find(item => item.file === rename.to));
435
+ const unstaged = this.getDiffStats(unstagedSummary.files.find(item => item.file === rename.to));
436
+ addChange(rename.to, 'R', staged.insertions + unstaged.insertions, staged.deletions + unstaged.deletions);
437
+ }
438
+ const totalInsertions = changes.reduce((total, change) => total + change.insertions, 0);
439
+ const totalDeletions = changes.reduce((total, change) => total + change.deletions, 0);
440
+ return {
441
+ repoName: path.basename(repoPath),
442
+ repoPath,
443
+ changes,
444
+ summary: {
445
+ totalFiles: changes.length,
446
+ totalInsertions,
447
+ totalDeletions,
448
+ totalChanges: totalInsertions + totalDeletions
449
+ }
450
+ };
451
+ }
452
+ async getGitChanges(requestedPath) {
453
+ try {
454
+ const rootPath = this.resolveWorkspacePath(requestedPath);
455
+ if (!fs.existsSync(rootPath)) {
456
+ return { success: false, error: `Path does not exist: ${rootPath}` };
457
+ }
458
+ const repoPaths = await this.findLocalGitRepositories(rootPath);
459
+ if (repoPaths.length === 0) {
460
+ return { success: false, error: `No git repositories found in: ${rootPath}` };
461
+ }
462
+ const repositories = [];
463
+ for (const repoPath of repoPaths) {
464
+ try {
465
+ repositories.push(await this.getLocalRepositoryChanges(repoPath));
466
+ }
467
+ catch (error) {
468
+ logger.error(`Failed to inspect repository ${repoPath}`, error);
469
+ }
470
+ }
471
+ const totalFiles = repositories.reduce((total, repo) => total + repo.summary.totalFiles, 0);
472
+ const totalInsertions = repositories.reduce((total, repo) => total + repo.summary.totalInsertions, 0);
473
+ const totalDeletions = repositories.reduce((total, repo) => total + repo.summary.totalDeletions, 0);
474
+ return {
475
+ success: true,
476
+ repositories,
477
+ overallSummary: {
478
+ totalRepositories: repositories.length,
479
+ totalFiles,
480
+ totalInsertions,
481
+ totalDeletions,
482
+ totalChanges: totalInsertions + totalDeletions
483
+ }
484
+ };
485
+ }
486
+ catch (error) {
487
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
488
+ }
489
+ }
490
+ async getFileDiff(repoPathOrName, filePath, statusCode) {
491
+ const repoPath = this.resolveLocalRepository(repoPathOrName);
492
+ const fullFilePath = this.resolveWorkspacePath(path.join(repoPath, filePath));
493
+ const relativeFilePath = path.relative(repoPath, fullFilePath);
494
+ if (relativeFilePath.startsWith('..') || path.isAbsolute(relativeFilePath)) {
495
+ throw new Error(`File is outside the repository: ${filePath}`);
496
+ }
497
+ const git = (0, simple_git_1.simpleGit)(repoPath);
498
+ let diff = '';
499
+ if (statusCode === 'MM') {
500
+ const staged = await git.diff(['--cached', relativeFilePath]);
501
+ const unstaged = await git.diff([relativeFilePath]);
502
+ diff = `--- STAGED CHANGES ---\n${staged}\n\n--- UNSTAGED CHANGES ---\n${unstaged}`;
503
+ }
504
+ else if (statusCode === 'A') {
505
+ diff = await git.diff(['--cached', relativeFilePath]);
506
+ }
507
+ else if (statusCode === '?') {
508
+ if (fs.existsSync(fullFilePath)) {
509
+ const lines = (await fs.promises.readFile(fullFilePath, 'utf8')).split('\n');
510
+ diff = `+++ b/${relativeFilePath}\n@@ -0,0 +1,${lines.length} @@\n${lines.map(line => `+${line}`).join('\n')}\n`;
511
+ }
512
+ }
513
+ else if (statusCode === 'D') {
514
+ const currentStatus = await git.status();
515
+ const args = currentStatus.staged.includes(relativeFilePath)
516
+ ? ['--cached', 'HEAD', relativeFilePath]
517
+ : ['HEAD', relativeFilePath];
518
+ diff = await git.diff(args).catch(() => '');
519
+ }
520
+ else {
521
+ diff = await git.diff([relativeFilePath]);
522
+ }
523
+ return { success: true, diff, filePath: relativeFilePath, repoPath };
524
+ }
525
+ async discardFileChanges(repoPathOrName, filePath) {
526
+ const repoPath = this.resolveLocalRepository(repoPathOrName);
527
+ const fullFilePath = this.resolveWorkspacePath(path.join(repoPath, filePath));
528
+ const relativeFilePath = path.relative(repoPath, fullFilePath);
529
+ if (relativeFilePath.startsWith('..') || path.isAbsolute(relativeFilePath)) {
530
+ throw new Error(`File is outside the repository: ${filePath}`);
531
+ }
532
+ const git = (0, simple_git_1.simpleGit)(repoPath);
533
+ const status = await git.status();
534
+ const isUntracked = status.not_added.includes(relativeFilePath) || (status.created || []).includes(relativeFilePath);
535
+ if (isUntracked) {
536
+ if (!fs.existsSync(fullFilePath)) {
537
+ return { success: false, error: `File '${relativeFilePath}' does not exist.` };
538
+ }
539
+ const stat = await fs.promises.lstat(fullFilePath);
540
+ if (!stat.isFile()) {
541
+ return { success: false, error: `Only untracked files can be discarded: ${relativeFilePath}` };
542
+ }
543
+ await fs.promises.unlink(fullFilePath);
544
+ return { success: true, message: `Untracked file '${relativeFilePath}' has been removed.` };
545
+ }
546
+ const hasChanges = status.modified.includes(relativeFilePath)
547
+ || status.staged.includes(relativeFilePath)
548
+ || status.deleted.includes(relativeFilePath);
549
+ if (!hasChanges) {
550
+ return { success: false, error: `File '${relativeFilePath}' has no changes to discard.` };
551
+ }
552
+ if (status.staged.includes(relativeFilePath)) {
553
+ await git.reset(['HEAD', relativeFilePath]);
554
+ }
555
+ await git.checkout(['HEAD', '--', relativeFilePath]);
556
+ return { success: true, message: `Changes to '${relativeFilePath}' have been discarded.` };
557
+ }
316
558
  escapeSingleQuotedShell(value) {
317
559
  return value.replace(/'/g, `'\\''`);
318
560
  }
@@ -439,21 +681,21 @@ class GithubOperationsService {
439
681
  try {
440
682
  logger.info('Starting git push operation', { repoName });
441
683
  // Combine all commands into a single SSH call
442
- const combinedCommand = `
443
- cd ${repoPath} &&
444
- BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
445
- COMMITS_AHEAD=$(sudo git rev-list --count origin/$BRANCH..HEAD 2>/dev/null || echo 0) &&
446
- if [ "$COMMITS_AHEAD" = "0" ]; then
447
- echo "ERROR:NO_COMMITS";
448
- exit 1;
449
- fi &&
450
- sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
451
- sudo git push origin $BRANCH &&
452
- sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
453
- COMMIT_HASH=$(sudo git rev-parse HEAD) &&
454
- echo "BRANCH:$BRANCH" &&
455
- echo "COMMITS_AHEAD:$COMMITS_AHEAD" &&
456
- echo "COMMIT_HASH:$COMMIT_HASH"
684
+ const combinedCommand = `
685
+ cd ${repoPath} &&
686
+ BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
687
+ COMMITS_AHEAD=$(sudo git rev-list --count origin/$BRANCH..HEAD 2>/dev/null || echo 0) &&
688
+ if [ "$COMMITS_AHEAD" = "0" ]; then
689
+ echo "ERROR:NO_COMMITS";
690
+ exit 1;
691
+ fi &&
692
+ sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
693
+ sudo git push origin $BRANCH &&
694
+ sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
695
+ COMMIT_HASH=$(sudo git rev-parse HEAD) &&
696
+ echo "BRANCH:$BRANCH" &&
697
+ echo "COMMITS_AHEAD:$COMMITS_AHEAD" &&
698
+ echo "COMMIT_HASH:$COMMIT_HASH"
457
699
  `.replace(/\n\s+/g, ' ');
458
700
  const result = await executeShell(combinedCommand);
459
701
  if (!result.success) {
@@ -496,23 +738,23 @@ class GithubOperationsService {
496
738
  try {
497
739
  logger.info('Checking repository status', { repoName });
498
740
  // Combine all git commands into a single SSH call
499
- const combinedCommand = `
500
- cd ${repoPath} &&
501
- BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
502
- sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
503
- sudo git fetch origin &&
504
- sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
505
- BEHIND=$(sudo git rev-list --count HEAD..origin/$BRANCH 2>/dev/null || echo 0) &&
506
- AHEAD=$(sudo git rev-list --count origin/$BRANCH..HEAD 2>/dev/null || echo 0) &&
507
- STATUS=$(sudo git status --porcelain) &&
508
- CURRENT_HASH=$(sudo git rev-parse HEAD) &&
509
- REMOTE_HASH=$(sudo git rev-parse origin/$BRANCH 2>/dev/null || echo unknown) &&
510
- echo "BRANCH:$BRANCH" &&
511
- echo "BEHIND:$BEHIND" &&
512
- echo "AHEAD:$AHEAD" &&
513
- echo "STATUS:$STATUS" &&
514
- echo "CURRENT:$CURRENT_HASH" &&
515
- echo "REMOTE:$REMOTE_HASH"
741
+ const combinedCommand = `
742
+ cd ${repoPath} &&
743
+ BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
744
+ sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
745
+ sudo git fetch origin &&
746
+ sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
747
+ BEHIND=$(sudo git rev-list --count HEAD..origin/$BRANCH 2>/dev/null || echo 0) &&
748
+ AHEAD=$(sudo git rev-list --count origin/$BRANCH..HEAD 2>/dev/null || echo 0) &&
749
+ STATUS=$(sudo git status --porcelain) &&
750
+ CURRENT_HASH=$(sudo git rev-parse HEAD) &&
751
+ REMOTE_HASH=$(sudo git rev-parse origin/$BRANCH 2>/dev/null || echo unknown) &&
752
+ echo "BRANCH:$BRANCH" &&
753
+ echo "BEHIND:$BEHIND" &&
754
+ echo "AHEAD:$AHEAD" &&
755
+ echo "STATUS:$STATUS" &&
756
+ echo "CURRENT:$CURRENT_HASH" &&
757
+ echo "REMOTE:$REMOTE_HASH"
516
758
  `.replace(/\n\s+/g, ' ');
517
759
  const result = await executeShell(combinedCommand);
518
760
  if (!result.success) {
@@ -709,23 +951,23 @@ class GithubOperationsService {
709
951
  const escapedAuthorName = commitAuthorName.replace(/"/g, '\\"');
710
952
  const escapedAuthorEmail = commitAuthorEmail.replace(/"/g, '\\"');
711
953
  // Combine all commands into a single SSH call
712
- const combinedCommand = `
713
- cd ${repoPath} &&
714
- sudo git config user.name "${escapedAuthorName}" &&
715
- sudo git config user.email "${escapedAuthorEmail}" &&
716
- STATUS=$(sudo git status --porcelain) &&
717
- if [ -z "$STATUS" ]; then
718
- echo "ERROR:NO_CHANGES";
719
- exit 1;
720
- fi &&
721
- CHANGED_FILES=$(echo "$STATUS" | wc -l) &&
722
- sudo git add . &&
723
- sudo git commit -m "${escapedCommitMsg}" &&
724
- COMMIT_HASH=$(sudo git rev-parse HEAD) &&
725
- SHORT_HASH=$(sudo git rev-parse --short HEAD) &&
726
- echo "CHANGED_FILES:$CHANGED_FILES" &&
727
- echo "COMMIT_HASH:$COMMIT_HASH" &&
728
- echo "SHORT_HASH:$SHORT_HASH"
954
+ const combinedCommand = `
955
+ cd ${repoPath} &&
956
+ sudo git config user.name "${escapedAuthorName}" &&
957
+ sudo git config user.email "${escapedAuthorEmail}" &&
958
+ STATUS=$(sudo git status --porcelain) &&
959
+ if [ -z "$STATUS" ]; then
960
+ echo "ERROR:NO_CHANGES";
961
+ exit 1;
962
+ fi &&
963
+ CHANGED_FILES=$(echo "$STATUS" | wc -l) &&
964
+ sudo git add . &&
965
+ sudo git commit -m "${escapedCommitMsg}" &&
966
+ COMMIT_HASH=$(sudo git rev-parse HEAD) &&
967
+ SHORT_HASH=$(sudo git rev-parse --short HEAD) &&
968
+ echo "CHANGED_FILES:$CHANGED_FILES" &&
969
+ echo "COMMIT_HASH:$COMMIT_HASH" &&
970
+ echo "SHORT_HASH:$SHORT_HASH"
729
971
  `.replace(/\n\s+/g, ' ');
730
972
  const result = await executeShell(combinedCommand);
731
973
  if (!result.success) {
@@ -810,20 +1052,20 @@ class GithubOperationsService {
810
1052
  try {
811
1053
  logger.info('Creating pull request', { repoName, targetBranch });
812
1054
  // Combine commands into a single SSH call, with remote URL set/unset
813
- const combinedCommand = `
814
- cd ${repoPath} &&
815
- sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
816
- HEAD_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
817
- sudo git fetch origin ${targetBranch} 2>/dev/null &&
818
- COMMITS_AHEAD=$(sudo git rev-list --count origin/${targetBranch}..$HEAD_BRANCH 2>/dev/null || echo 0) &&
819
- if [ "$COMMITS_AHEAD" = "0" ]; then
820
- echo "ERROR:NO_COMMITS:$HEAD_BRANCH";
821
- sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git;
822
- exit 1;
823
- fi &&
824
- echo "HEAD_BRANCH:$HEAD_BRANCH" &&
825
- echo "COMMITS_AHEAD:$COMMITS_AHEAD" &&
826
- sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git
1055
+ const combinedCommand = `
1056
+ cd ${repoPath} &&
1057
+ sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
1058
+ HEAD_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
1059
+ sudo git fetch origin ${targetBranch} 2>/dev/null &&
1060
+ COMMITS_AHEAD=$(sudo git rev-list --count origin/${targetBranch}..$HEAD_BRANCH 2>/dev/null || echo 0) &&
1061
+ if [ "$COMMITS_AHEAD" = "0" ]; then
1062
+ echo "ERROR:NO_COMMITS:$HEAD_BRANCH";
1063
+ sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git;
1064
+ exit 1;
1065
+ fi &&
1066
+ echo "HEAD_BRANCH:$HEAD_BRANCH" &&
1067
+ echo "COMMITS_AHEAD:$COMMITS_AHEAD" &&
1068
+ sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git
827
1069
  `.replace(/\n\s+/g, ' ');
828
1070
  let result = await executeShell(combinedCommand);
829
1071
  const output = result.output || '';
@@ -1020,20 +1262,20 @@ class GithubOperationsService {
1020
1262
  const targetLocalBranch = resolvedTarget.displayBranch;
1021
1263
  const safeTargetLocalBranch = this.escapeSingleQuotedShell(targetLocalBranch);
1022
1264
  const safeSourceRef = this.escapeSingleQuotedShell(resolvedSource.resolvedRef);
1023
- const prepareBranchCommand = `
1024
- cd ${repoPath} &&
1025
- TARGET_LOCAL='${safeTargetLocalBranch}' &&
1026
- if sudo git show-ref --verify --quiet "refs/heads/$TARGET_LOCAL"; then
1027
- sudo git checkout "$TARGET_LOCAL";
1028
- elif sudo git show-ref --verify --quiet "refs/remotes/origin/$TARGET_LOCAL"; then
1029
- sudo git checkout -b "$TARGET_LOCAL" "origin/$TARGET_LOCAL";
1030
- else
1031
- echo "ERROR:TARGET_BRANCH_NOT_CHECKOUTABLE";
1032
- exit 1;
1033
- fi &&
1034
- if sudo git show-ref --verify --quiet "refs/remotes/origin/$TARGET_LOCAL"; then
1035
- sudo git pull --ff-only origin "$TARGET_LOCAL" 2>/dev/null || true;
1036
- fi
1265
+ const prepareBranchCommand = `
1266
+ cd ${repoPath} &&
1267
+ TARGET_LOCAL='${safeTargetLocalBranch}' &&
1268
+ if sudo git show-ref --verify --quiet "refs/heads/$TARGET_LOCAL"; then
1269
+ sudo git checkout "$TARGET_LOCAL";
1270
+ elif sudo git show-ref --verify --quiet "refs/remotes/origin/$TARGET_LOCAL"; then
1271
+ sudo git checkout -b "$TARGET_LOCAL" "origin/$TARGET_LOCAL";
1272
+ else
1273
+ echo "ERROR:TARGET_BRANCH_NOT_CHECKOUTABLE";
1274
+ exit 1;
1275
+ fi &&
1276
+ if sudo git show-ref --verify --quiet "refs/remotes/origin/$TARGET_LOCAL"; then
1277
+ sudo git pull --ff-only origin "$TARGET_LOCAL" 2>/dev/null || true;
1278
+ fi
1037
1279
  `.replace(/\n\s+/g, ' ');
1038
1280
  result = await executeShell(prepareBranchCommand);
1039
1281
  if (!result.success) {
@@ -1131,16 +1373,16 @@ class GithubOperationsService {
1131
1373
  try {
1132
1374
  logger.info('Checking if PR exists', { repoName, targetBranch });
1133
1375
  // Get current branch and commits ahead
1134
- const combinedCommand = `
1135
- cd ${repoPath} &&
1136
- sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
1137
- HEAD_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
1138
- sudo git fetch origin --prune 2>/dev/null || true &&
1139
- sudo git fetch origin ${targetBranch} 2>/dev/null || true &&
1140
- COMMITS_AHEAD=$(sudo git rev-list --count origin/${targetBranch}..$HEAD_BRANCH 2>/dev/null || echo 0) &&
1141
- sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
1142
- echo "HEAD_BRANCH:$HEAD_BRANCH" &&
1143
- echo "COMMITS_AHEAD:$COMMITS_AHEAD"
1376
+ const combinedCommand = `
1377
+ cd ${repoPath} &&
1378
+ sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
1379
+ HEAD_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
1380
+ sudo git fetch origin --prune 2>/dev/null || true &&
1381
+ sudo git fetch origin ${targetBranch} 2>/dev/null || true &&
1382
+ COMMITS_AHEAD=$(sudo git rev-list --count origin/${targetBranch}..$HEAD_BRANCH 2>/dev/null || echo 0) &&
1383
+ sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
1384
+ echo "HEAD_BRANCH:$HEAD_BRANCH" &&
1385
+ echo "COMMITS_AHEAD:$COMMITS_AHEAD"
1144
1386
  `.replace(/\n\s+/g, ' ');
1145
1387
  const result = await executeShell(combinedCommand);
1146
1388
  // Check for command execution failure
@@ -1415,14 +1657,14 @@ class GithubOperationsService {
1415
1657
  try {
1416
1658
  logger.info('Fetching commit details', { repoName, commitHash });
1417
1659
  // Get commit information and file changes in a single SSH call
1418
- const combinedCommand = `
1419
- cd ${repoPath} &&
1420
- COMMIT_INFO=$(sudo git show --no-patch --pretty=format:"%H|%h|%an|%ae|%ad|%s" --date=iso ${commitHash}) &&
1421
- FILE_STATS=$(sudo git show --stat --pretty="" --numstat ${commitHash}) &&
1422
- echo "COMMIT_INFO:$COMMIT_INFO" &&
1423
- echo "FILE_STATS_START" &&
1424
- echo "$FILE_STATS" &&
1425
- echo "FILE_STATS_END"
1660
+ const combinedCommand = `
1661
+ cd ${repoPath} &&
1662
+ COMMIT_INFO=$(sudo git show --no-patch --pretty=format:"%H|%h|%an|%ae|%ad|%s" --date=iso ${commitHash}) &&
1663
+ FILE_STATS=$(sudo git show --stat --pretty="" --numstat ${commitHash}) &&
1664
+ echo "COMMIT_INFO:$COMMIT_INFO" &&
1665
+ echo "FILE_STATS_START" &&
1666
+ echo "$FILE_STATS" &&
1667
+ echo "FILE_STATS_END"
1426
1668
  `.replace(/\n\s+/g, ' ');
1427
1669
  const result = await executeShell(combinedCommand);
1428
1670
  if (!result.success) {
@@ -1716,179 +1958,6 @@ class GithubOperationsService {
1716
1958
  });
1717
1959
  });
1718
1960
  }
1719
- // ── New git operations ─────────────────────────────────────────────────────
1720
- /**
1721
- * Get all uncommitted git changes (metadata only, no diffs) for a repository.
1722
- * Accepts repoName (looked up under folderPath) or a full absolute repoPath.
1723
- */
1724
- async getGitChanges(repoName) {
1725
- const pathModule = await Promise.resolve().then(() => __importStar(require('path')));
1726
- const fs = await Promise.resolve().then(() => __importStar(require('fs')));
1727
- logger.info(`[getGitChanges] repoName: ${repoName}`);
1728
- try {
1729
- // Resolve path: absolute as-is, otherwise join with folderPath
1730
- const repoPath = pathModule.isAbsolute(repoName)
1731
- ? repoName
1732
- : pathModule.join(tool_server_1.folderPath, repoName);
1733
- if (!fs.existsSync(repoPath)) {
1734
- return { success: false, error: `Path does not exist: ${repoPath}` };
1735
- }
1736
- // Case 1: the path itself is a git repository
1737
- const gitDir = pathModule.join(repoPath, '.git');
1738
- if (fs.existsSync(gitDir)) {
1739
- logger.info(`[getGitChanges] Single repository detected: ${repoPath}`);
1740
- const repoData = await getRepositoryChangesMetadataOnly(repoPath);
1741
- return {
1742
- success: true,
1743
- repositories: [repoData],
1744
- overallSummary: {
1745
- totalRepositories: 1,
1746
- totalFiles: repoData.summary.totalFiles,
1747
- totalInsertions: repoData.summary.totalInsertions,
1748
- totalDeletions: repoData.summary.totalDeletions,
1749
- totalChanges: repoData.summary.totalChanges
1750
- }
1751
- };
1752
- }
1753
- // Case 2: not a git repo directly — search recursively for git repos inside
1754
- logger.info(`[getGitChanges] Not a git repo, searching recursively in: ${repoPath}`);
1755
- const gitRepositories = await findGitRepositories(repoPath);
1756
- if (gitRepositories.length === 0) {
1757
- return { success: false, error: `No git repositories found in: ${repoPath}` };
1758
- }
1759
- logger.info(`[getGitChanges] Found ${gitRepositories.length} git repositories`);
1760
- const repositories = [];
1761
- let overallFiles = 0, overallInsertions = 0, overallDeletions = 0, overallChanges = 0;
1762
- for (const gitRepoPath of gitRepositories) {
1763
- try {
1764
- const repoData = await getRepositoryChangesMetadataOnly(gitRepoPath);
1765
- repositories.push(repoData);
1766
- overallFiles += repoData.summary.totalFiles;
1767
- overallInsertions += repoData.summary.totalInsertions;
1768
- overallDeletions += repoData.summary.totalDeletions;
1769
- overallChanges += repoData.summary.totalChanges;
1770
- logger.info(`[getGitChanges] ${repoData.repoName}: ${repoData.summary.totalFiles} files changed`);
1771
- }
1772
- catch (repoError) {
1773
- logger.error(`[getGitChanges] Error processing repo ${gitRepoPath}:`, repoError);
1774
- // continue with other repos
1775
- }
1776
- }
1777
- return {
1778
- success: true,
1779
- repositories,
1780
- overallSummary: {
1781
- totalRepositories: repositories.length,
1782
- totalFiles: overallFiles,
1783
- totalInsertions: overallInsertions,
1784
- totalDeletions: overallDeletions,
1785
- totalChanges: overallChanges
1786
- }
1787
- };
1788
- }
1789
- catch (error) {
1790
- logger.error('[getGitChanges] Error:', error);
1791
- return {
1792
- success: false,
1793
- error: error instanceof Error ? error.message : 'Unknown error occurred while getting git changes'
1794
- };
1795
- }
1796
- }
1797
- /**
1798
- * Get the unified diff for a single file in a repository.
1799
- * status codes: 'M' modified, 'MM' modified+staged, 'A' added/staged,
1800
- * '?' untracked, 'D' deleted, 'R' renamed
1801
- */
1802
- async getFileDiff(repoName, filePath, status) {
1803
- const pathModule = await Promise.resolve().then(() => __importStar(require('path')));
1804
- const fs = await Promise.resolve().then(() => __importStar(require('fs')));
1805
- logger.info(`[getFileDiff] repoName: ${repoName} | filePath: ${filePath} | status: ${status}`);
1806
- try {
1807
- let repoPath = pathModule.isAbsolute(repoName)
1808
- ? repoName
1809
- : pathModule.join(tool_server_1.folderPath, repoName);
1810
- if (!fs.existsSync(repoPath)) {
1811
- return { success: false, error: `Repository path does not exist: ${repoPath}`, filePath, repoPath };
1812
- }
1813
- const result = await getSingleFileDiff(repoPath, filePath, status);
1814
- logger.success(`[getFileDiff] Retrieved diff for ${filePath} (${(result.diff || '').length} chars)`);
1815
- return result;
1816
- }
1817
- catch (error) {
1818
- logger.error('[getFileDiff] Error:', error);
1819
- return {
1820
- success: false,
1821
- error: error instanceof Error ? error.message : 'Unknown error occurred while getting file diff',
1822
- filePath,
1823
- repoPath: repoName
1824
- };
1825
- }
1826
- }
1827
- /**
1828
- * Discard changes for a single file in a repository.
1829
- * - Untracked / created files: physically deleted from disk.
1830
- * - Modified / staged / deleted files: unstaged if needed, then restored from HEAD.
1831
- */
1832
- async discardFileChanges(repoName, filePath) {
1833
- const pathModule = await Promise.resolve().then(() => __importStar(require('path')));
1834
- const fs = await Promise.resolve().then(() => __importStar(require('fs')));
1835
- logger.info(`[discardFileChanges] repoName: ${repoName} | filePath: ${filePath}`);
1836
- try {
1837
- const repoPath = pathModule.isAbsolute(repoName)
1838
- ? repoName
1839
- : pathModule.join(tool_server_1.folderPath, repoName);
1840
- if (!fs.existsSync(repoPath)) {
1841
- return { success: false, error: `Repository path does not exist: ${repoPath}` };
1842
- }
1843
- const gitDir = pathModule.join(repoPath, '.git');
1844
- if (!fs.existsSync(gitDir)) {
1845
- return { success: false, error: `Not a git repository: ${repoPath}` };
1846
- }
1847
- const git = (0, simple_git_1.simpleGit)(repoPath);
1848
- const status = await git.status();
1849
- const isModified = status.modified.includes(filePath);
1850
- const isStaged = status.staged.includes(filePath);
1851
- const isDeleted = status.deleted.includes(filePath);
1852
- const isCreated = status.not_added.includes(filePath) || ((status.created || []).includes(filePath));
1853
- const fullFilePath = pathModule.join(repoPath, filePath);
1854
- const fileExistsOnDisk = fs.existsSync(fullFilePath);
1855
- logger.info(`[discardFileChanges] flags — modified:${isModified} staged:${isStaged} deleted:${isDeleted} created:${isCreated} existsOnDisk:${fileExistsOnDisk}`);
1856
- if (isCreated) {
1857
- // Untracked file — remove from disk
1858
- if (fileExistsOnDisk) {
1859
- await fs.promises.unlink(fullFilePath);
1860
- logger.success(`[discardFileChanges] Untracked file removed: ${filePath}`);
1861
- return { success: true, message: `Untracked file '${filePath}' has been removed.` };
1862
- }
1863
- else {
1864
- return { success: false, error: `File '${filePath}' does not exist on disk.` };
1865
- }
1866
- }
1867
- if (isModified || isStaged || isDeleted) {
1868
- try {
1869
- if (isStaged) {
1870
- await git.reset(['HEAD', filePath]);
1871
- logger.info(`[discardFileChanges] Unstaged: ${filePath}`);
1872
- }
1873
- await git.checkout(['HEAD', '--', filePath]);
1874
- logger.success(`[discardFileChanges] Restored from HEAD: ${filePath}`);
1875
- return { success: true, message: `Changes to '${filePath}' have been discarded.` };
1876
- }
1877
- catch (checkoutError) {
1878
- logger.error('[discardFileChanges] git checkout error:', checkoutError);
1879
- return { success: false, error: `Failed to discard changes: ${checkoutError.message}` };
1880
- }
1881
- }
1882
- return { success: false, error: `File '${filePath}' has no changes to discard.` };
1883
- }
1884
- catch (error) {
1885
- logger.error('[discardFileChanges] Error:', error);
1886
- return {
1887
- success: false,
1888
- error: error instanceof Error ? error.message : 'Unknown error occurred while discarding changes'
1889
- };
1890
- }
1891
- }
1892
1961
  }
1893
1962
  exports.GithubOperationsService = GithubOperationsService;
1894
1963
  //# sourceMappingURL=githubOperationsHanlder.js.map