phantomx-tool-client 1.0.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.
Files changed (34) hide show
  1. package/bin/tool-server.js +16 -0
  2. package/dist/Services/EditTool.d.ts +86 -0
  3. package/dist/Services/EditTool.d.ts.map +1 -0
  4. package/dist/Services/EditTool.js +516 -0
  5. package/dist/Services/EditTool.js.map +1 -0
  6. package/dist/Services/Logger.d.ts +32 -0
  7. package/dist/Services/Logger.d.ts.map +1 -0
  8. package/dist/Services/Logger.js +185 -0
  9. package/dist/Services/Logger.js.map +1 -0
  10. package/dist/Services/ReadFileTool.d.ts +49 -0
  11. package/dist/Services/ReadFileTool.d.ts.map +1 -0
  12. package/dist/Services/ReadFileTool.js +239 -0
  13. package/dist/Services/ReadFileTool.js.map +1 -0
  14. package/dist/__tests__/__mocks__/tool-server.mock.d.ts +4 -0
  15. package/dist/__tests__/__mocks__/tool-server.mock.d.ts.map +1 -0
  16. package/dist/__tests__/__mocks__/tool-server.mock.js +8 -0
  17. package/dist/__tests__/__mocks__/tool-server.mock.js.map +1 -0
  18. package/dist/__tests__/toolExecutionService_tempAI.test.d.ts +13 -0
  19. package/dist/__tests__/toolExecutionService_tempAI.test.d.ts.map +1 -0
  20. package/dist/__tests__/toolExecutionService_tempAI.test.js +491 -0
  21. package/dist/__tests__/toolExecutionService_tempAI.test.js.map +1 -0
  22. package/dist/githubOperationsHanlder.d.ts +274 -0
  23. package/dist/githubOperationsHanlder.d.ts.map +1 -0
  24. package/dist/githubOperationsHanlder.js +1487 -0
  25. package/dist/githubOperationsHanlder.js.map +1 -0
  26. package/dist/tool-server.d.ts +4 -0
  27. package/dist/tool-server.d.ts.map +1 -0
  28. package/dist/tool-server.js +190 -0
  29. package/dist/tool-server.js.map +1 -0
  30. package/dist/toolExecutionService.d.ts +28 -0
  31. package/dist/toolExecutionService.d.ts.map +1 -0
  32. package/dist/toolExecutionService.js +464 -0
  33. package/dist/toolExecutionService.js.map +1 -0
  34. package/package.json +58 -0
@@ -0,0 +1,1487 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.GithubOperationsService = void 0;
37
+ exports.getGithubOrganizationName = getGithubOrganizationName;
38
+ exports.fetchInstallationToken = fetchInstallationToken;
39
+ exports.getRepoList = getRepoList;
40
+ exports.getRepoBranch = getRepoBranch;
41
+ const Logger_1 = require("./Services/Logger");
42
+ const child_process_1 = require("child_process");
43
+ // ---------------------------------------------------------------------------
44
+ // Local bash executor — drop-in replacement for ssh.executeCommand()
45
+ // Returns { success, output, error, code } matching the SSH client response.
46
+ // ---------------------------------------------------------------------------
47
+ function executeShell(command) {
48
+ return new Promise((resolve) => {
49
+ const child = (0, child_process_1.spawn)('bash', ['-c', command], {
50
+ env: process.env,
51
+ stdio: ['ignore', 'pipe', 'pipe']
52
+ });
53
+ let stdout = '';
54
+ let stderr = '';
55
+ child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
56
+ child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
57
+ child.on('close', (code) => {
58
+ resolve({ success: code === 0, output: stdout, error: stderr, code: code !== null && code !== void 0 ? code : 1 });
59
+ });
60
+ child.on('error', (err) => {
61
+ resolve({ success: false, output: '', error: err.message, code: 1 });
62
+ });
63
+ });
64
+ }
65
+ // Use the official 'octokit' package import via require to avoid missing types for '@octokit/rest'
66
+ const { Octokit } = require('octokit');
67
+ const logger = (0, Logger_1.createLogger)('GithubOperationsService');
68
+ const simple_git_1 = require("simple-git");
69
+ const tool_server_1 = require("./tool-server");
70
+ async function getGithubOrganizationName() {
71
+ return tool_server_1.githubOrgName;
72
+ }
73
+ async function fetchInstallationToken() {
74
+ // this is not the installation token but a github PAT token for accessing the github repos
75
+ // fetching this token from an .env
76
+ return tool_server_1.githubPATToken;
77
+ }
78
+ async function getRepoList() {
79
+ let installationToken = await fetchInstallationToken(); //getting the installation Token
80
+ const resp = await fetch("https://api.github.com/installation/repositories", {
81
+ headers: {
82
+ Accept: "application/vnd.github+json",
83
+ Authorization: `Bearer ${installationToken}`,
84
+ "X-GitHub-Api-Version": "2022-11-28",
85
+ },
86
+ });
87
+ if (!resp.ok) {
88
+ throw new Error(`Failed to list repos: ${resp.status} ${await resp.text()}`);
89
+ }
90
+ const data = await resp.json();
91
+ // data.repositories is an array of repo objects
92
+ return data.repositories.map((repo) => repo.name);
93
+ }
94
+ async function getRepoBranch(owner, repo) {
95
+ const installationToken = (await fetchInstallationToken()); //getting the installation Token
96
+ const octokit = new Octokit({ auth: installationToken });
97
+ try {
98
+ const branches = await octokit.paginate(octokit.rest.repos.listBranches, {
99
+ owner,
100
+ repo,
101
+ per_page: 100
102
+ });
103
+ return branches;
104
+ }
105
+ catch (err) {
106
+ console.log("Error listing branches:", err);
107
+ throw err;
108
+ }
109
+ }
110
+ class GithubOperationsService {
111
+ escapeSingleQuotedShell(value) {
112
+ return value.replace(/'/g, `'\\''`);
113
+ }
114
+ async resolveBranchReference(repoPath, repoName, branchName) {
115
+ const requestedBranch = (branchName || '').trim();
116
+ if (!requestedBranch) {
117
+ throw new Error('Branch name is required');
118
+ }
119
+ const candidateRefs = requestedBranch.startsWith('origin/')
120
+ ? [requestedBranch, requestedBranch.replace(/^origin\//, '')]
121
+ : [requestedBranch, `origin/${requestedBranch}`];
122
+ const uniqueCandidates = Array.from(new Set(candidateRefs.filter((candidate) => candidate.trim().length > 0)));
123
+ for (const candidate of uniqueCandidates) {
124
+ const safeCandidate = this.escapeSingleQuotedShell(candidate);
125
+ const branchCheck = await executeShell(`cd ${repoPath} && sudo git rev-parse --verify '${safeCandidate}^{commit}' >/dev/null 2>&1 && echo "BRANCH_OK"`);
126
+ if (branchCheck.success && (branchCheck.output || '').includes('BRANCH_OK')) {
127
+ return {
128
+ requestedBranch,
129
+ resolvedRef: candidate,
130
+ displayBranch: candidate.replace(/^origin\//, '')
131
+ };
132
+ }
133
+ }
134
+ throw new Error(`Branch '${requestedBranch}' not found in repository '${repoName}'`);
135
+ }
136
+ /**
137
+ * Get repository list for an installation
138
+ */
139
+ async getRepositoryList() {
140
+ const repositories = await getRepoList();
141
+ return {
142
+ repositories,
143
+ count: repositories.length
144
+ };
145
+ }
146
+ /**
147
+ * List branches for a repository
148
+ */
149
+ async listBranches(owner, repo) {
150
+ logger.info(`Fetching branches for ${owner}/${repo} )`);
151
+ const branches = await getRepoBranch(owner, repo);
152
+ return {
153
+ repo,
154
+ branches,
155
+ count: branches.length
156
+ };
157
+ }
158
+ /**
159
+ * Pull repository from remote
160
+ */
161
+ async pullRepository(repoName, socket) {
162
+ const repoPath = `${tool_server_1.folderPath}/${repoName}`;
163
+ const installationToken = await fetchInstallationToken();
164
+ const gitOrgName = await getGithubOrganizationName();
165
+ try {
166
+ // Get current local branch name
167
+ let result = await executeShell(`cd ${repoPath} && sudo git rev-parse --abbrev-ref HEAD`);
168
+ if (!result.success) {
169
+ throw new Error(`Failed to get current branch: ${result.error}`);
170
+ }
171
+ const currentBranch = result.output.trim();
172
+ logger.info('starting git pull operation', { repoName, currentBranch });
173
+ // Set remote url with token
174
+ result = await executeShell(`cd ${repoPath} && sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git`);
175
+ if (!result.success) {
176
+ throw new Error(`Failed to set remote URL: ${result.error}`);
177
+ }
178
+ result = await executeShell(`cd ${repoPath} && sudo git fetch origin`);
179
+ if (!result.success) {
180
+ throw new Error(`Failed to fetch from remote: ${result.error}`);
181
+ }
182
+ result = await executeShell(`cd ${repoPath} && sudo git pull --no-rebase origin ${currentBranch}`);
183
+ if (!result.success && result.code !== 128) {
184
+ throw new Error(`Failed to pull branch ${currentBranch}: ${result.error}`);
185
+ }
186
+ await executeShell(`cd ${repoPath} && sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git`);
187
+ logger.success('Git pull completed successfully', { repoName, currentBranch });
188
+ // Get merge conflicts
189
+ const mergeConflicts = await this.getMergeConflicts(socket, repoName);
190
+ return {
191
+ repoName,
192
+ branchName: currentBranch,
193
+ lastCommit: result.output.trim(),
194
+ mergeConflicts
195
+ };
196
+ }
197
+ finally {
198
+ }
199
+ }
200
+ /**
201
+ * Push repository to remote
202
+ */
203
+ async pushRepository(repoName, socket) {
204
+ const repoPath = `${tool_server_1.folderPath}/${repoName}`;
205
+ const installationToken = await fetchInstallationToken();
206
+ const gitOrgName = await getGithubOrganizationName();
207
+ try {
208
+ logger.info('Starting git push operation', { repoName });
209
+ // Combine all commands into a single SSH call
210
+ const combinedCommand = `
211
+ cd ${repoPath} &&
212
+ BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
213
+ COMMITS_AHEAD=$(sudo git rev-list --count origin/$BRANCH..HEAD 2>/dev/null || echo 0) &&
214
+ if [ "$COMMITS_AHEAD" = "0" ]; then
215
+ echo "ERROR:NO_COMMITS";
216
+ exit 1;
217
+ fi &&
218
+ sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
219
+ sudo git push origin $BRANCH &&
220
+ sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
221
+ COMMIT_HASH=$(sudo git rev-parse HEAD) &&
222
+ echo "BRANCH:$BRANCH" &&
223
+ echo "COMMITS_AHEAD:$COMMITS_AHEAD" &&
224
+ echo "COMMIT_HASH:$COMMIT_HASH"
225
+ `.replace(/\n\s+/g, ' ');
226
+ const result = await executeShell(combinedCommand);
227
+ if (!result.success) {
228
+ if (result.output && result.output.includes('ERROR:NO_COMMITS')) {
229
+ throw new Error('No commits to push');
230
+ }
231
+ throw new Error('Failed to push repository');
232
+ }
233
+ // Parse output
234
+ const output = result.output;
235
+ const branchMatch = output.match(/BRANCH:([^\n]+)/);
236
+ const commitsAheadMatch = output.match(/COMMITS_AHEAD:(\d+)/);
237
+ const commitHashMatch = output.match(/COMMIT_HASH:([a-f0-9]+)/);
238
+ const currentBranch = branchMatch ? branchMatch[1].trim() : 'unknown';
239
+ const commitsAhead = commitsAheadMatch ? parseInt(commitsAheadMatch[1]) : 0;
240
+ const commitHash = commitHashMatch ? commitHashMatch[1].trim() : 'unknown';
241
+ logger.success('Git push completed successfully', { repoName, currentBranch, commitHash, commitsAhead });
242
+ // Emit socket event to refresh repo on client (only when called by AI agent)
243
+ if (socket) {
244
+ socket.emit('refresh_repo', { repoName });
245
+ }
246
+ return {
247
+ repoName,
248
+ branchName: currentBranch,
249
+ commitHash,
250
+ commitsAhead,
251
+ pushedAt: new Date().toISOString()
252
+ };
253
+ }
254
+ finally {
255
+ }
256
+ }
257
+ /**
258
+ * Check repository status
259
+ */
260
+ async checkRepositoryStatus(repoName, socket) {
261
+ const repoPath = `${tool_server_1.folderPath}/${repoName}`;
262
+ const installationToken = await fetchInstallationToken();
263
+ const gitOrgName = await getGithubOrganizationName();
264
+ try {
265
+ logger.info('Checking repository status', { repoName });
266
+ // Combine all git commands into a single SSH call
267
+ const combinedCommand = `
268
+ cd ${repoPath} &&
269
+ BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
270
+ sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
271
+ sudo git fetch origin &&
272
+ sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
273
+ BEHIND=$(sudo git rev-list --count HEAD..origin/$BRANCH 2>/dev/null || echo 0) &&
274
+ AHEAD=$(sudo git rev-list --count origin/$BRANCH..HEAD 2>/dev/null || echo 0) &&
275
+ STATUS=$(sudo git status --porcelain) &&
276
+ CURRENT_HASH=$(sudo git rev-parse HEAD) &&
277
+ REMOTE_HASH=$(sudo git rev-parse origin/$BRANCH 2>/dev/null || echo unknown) &&
278
+ echo "BRANCH:$BRANCH" &&
279
+ echo "BEHIND:$BEHIND" &&
280
+ echo "AHEAD:$AHEAD" &&
281
+ echo "STATUS:$STATUS" &&
282
+ echo "CURRENT:$CURRENT_HASH" &&
283
+ echo "REMOTE:$REMOTE_HASH"
284
+ `.replace(/\n\s+/g, ' ');
285
+ const result = await executeShell(combinedCommand);
286
+ if (!result.success) {
287
+ throw new Error('Failed to check repository status');
288
+ }
289
+ // Parse the output
290
+ const output = result.output;
291
+ const branchMatch = output.match(/BRANCH:([^\n]+)/);
292
+ const behindMatch = output.match(/BEHIND:(\d+)/);
293
+ const aheadMatch = output.match(/AHEAD:(\d+)/);
294
+ const statusMatch = output.match(/STATUS:([\s\S]*?)(?=CURRENT:|$)/);
295
+ const currentMatch = output.match(/CURRENT:([a-f0-9]+)/);
296
+ const remoteMatch = output.match(/REMOTE:([a-f0-9]+|unknown)/);
297
+ const currentBranch = branchMatch ? branchMatch[1].trim() : 'unknown';
298
+ const commitsBehind = behindMatch ? parseInt(behindMatch[1]) : 0;
299
+ const commitsAhead = aheadMatch ? parseInt(aheadMatch[1]) : 0;
300
+ const statusOutput = statusMatch ? statusMatch[1].trim() : '';
301
+ const hasUncommittedChanges = statusOutput.length > 0;
302
+ const currentCommitHash = currentMatch ? currentMatch[1].trim() : 'unknown';
303
+ const remoteCommitHash = remoteMatch ? remoteMatch[1].trim() : 'unknown';
304
+ const isUpToDate = commitsBehind === 0 && commitsAhead === 0;
305
+ const needsPull = commitsBehind > 0;
306
+ const needsPush = commitsAhead > 0;
307
+ logger.info('Repository status checked', {
308
+ repoName,
309
+ commitsBehind,
310
+ commitsAhead,
311
+ hasUncommittedChanges,
312
+ isUpToDate
313
+ });
314
+ // Check for merge conflicts
315
+ const mergeConflicts = await this.getMergeConflicts(socket, repoName);
316
+ return {
317
+ repoName,
318
+ branchName: currentBranch,
319
+ currentCommitHash,
320
+ remoteCommitHash,
321
+ commitsBehind,
322
+ commitsAhead,
323
+ hasUncommittedChanges,
324
+ isUpToDate,
325
+ needsPull,
326
+ needsPush,
327
+ mergeConflicts,
328
+ status: isUpToDate ? 'up-to-date' : (needsPull && needsPush ? 'diverged' : needsPull ? 'behind' : 'ahead')
329
+ };
330
+ }
331
+ finally {
332
+ }
333
+ }
334
+ /**
335
+ * Get repository history
336
+ */
337
+ async getRepositoryHistory(repoName, remoteBranchName, limit = 50) {
338
+ const repoPath = `${tool_server_1.folderPath}/${repoName}`;
339
+ try {
340
+ logger.info('Fetching repository history', { repoName, remoteBranchName, limit });
341
+ const safeLimit = Number.isFinite(limit) ? Math.min(Math.max(limit, 1), 500) : 50;
342
+ let targetBranch = (remoteBranchName || '').trim();
343
+ if (!targetBranch) {
344
+ const branchResult = await executeShell(`cd ${repoPath} && sudo git rev-parse --abbrev-ref HEAD`);
345
+ if (!branchResult.success) {
346
+ throw new Error(`Failed to resolve current branch: ${branchResult.error || branchResult.output || 'Unknown error'}`);
347
+ }
348
+ targetBranch = branchResult.output.trim();
349
+ }
350
+ const resolvedBranch = await this.resolveBranchReference(repoPath, repoName, targetBranch);
351
+ const safeResolvedRef = this.escapeSingleQuotedShell(resolvedBranch.resolvedRef);
352
+ // Get reflog with detailed information
353
+ let result = await executeShell(`cd ${repoPath} && sudo git reflog -${safeLimit} --date=iso`);
354
+ if (!result.success) {
355
+ throw new Error(`Failed to fetch reflog: ${result.error}`);
356
+ }
357
+ const reflogEntries = result.output.trim().split('\n').filter((line) => line.trim());
358
+ // Get commit log with detailed information
359
+ result = await executeShell(`cd ${repoPath} && sudo git log '${safeResolvedRef}' -${safeLimit} --pretty=format:"%H|%an|%ae|%ad|%s" --date=iso`);
360
+ if (!result.success) {
361
+ throw new Error(`Failed to fetch commit log for branch '${resolvedBranch.displayBranch}': ${result.error || result.output || 'Unknown error'}`);
362
+ }
363
+ const commitLog = result.output.trim() ? result.output.trim().split('\n').filter((line) => line.trim()) : [];
364
+ // Parse reflog entries
365
+ const parsedReflog = reflogEntries.map((entry) => {
366
+ // Format: hash HEAD@{n}: action: message
367
+ const match = entry.match(/^([a-f0-9]+)\s+HEAD@\{(\d+)\}:\s+(.+?):\s+(.+)$/);
368
+ if (match) {
369
+ const [, hash, index, action, message] = match;
370
+ // Determine action type
371
+ let actionType = 'unknown';
372
+ let actionDetails = {};
373
+ if (action.includes('commit')) {
374
+ actionType = 'commit';
375
+ actionDetails = { type: action.includes('initial') ? 'initial' : 'regular' };
376
+ }
377
+ else if (action.includes('pull')) {
378
+ actionType = 'pull';
379
+ const branchMatch = message.match(/from (.+)/);
380
+ actionDetails = { source: branchMatch ? branchMatch[1] : 'unknown' };
381
+ }
382
+ else if (action.includes('merge')) {
383
+ actionType = 'merge';
384
+ actionDetails = { message: message };
385
+ }
386
+ else if (action.includes('checkout')) {
387
+ actionType = 'checkout';
388
+ const branchMatch = message.match(/to (.+)/);
389
+ actionDetails = { branch: branchMatch ? branchMatch[1] : message };
390
+ }
391
+ else if (action.includes('rebase')) {
392
+ actionType = 'rebase';
393
+ }
394
+ else if (action.includes('reset')) {
395
+ actionType = 'reset';
396
+ actionDetails = { target: message };
397
+ }
398
+ else if (action.includes('clone')) {
399
+ actionType = 'clone';
400
+ }
401
+ return {
402
+ hash: hash.substring(0, 7),
403
+ fullHash: hash,
404
+ index: parseInt(index),
405
+ action: actionType,
406
+ actionRaw: action,
407
+ message: message,
408
+ details: actionDetails
409
+ };
410
+ }
411
+ return null;
412
+ }).filter((entry) => entry !== null);
413
+ // Parse commit log
414
+ const parsedCommits = commitLog.map((commit) => {
415
+ const [hash, author, email, date, message] = commit.split('|');
416
+ return {
417
+ hash: hash.substring(0, 7),
418
+ fullHash: hash,
419
+ author,
420
+ email,
421
+ date,
422
+ message
423
+ };
424
+ });
425
+ const currentBranch = resolvedBranch.displayBranch;
426
+ // Get latest commit info
427
+ result = await executeShell(`cd ${repoPath} && sudo git rev-parse '${safeResolvedRef}'`);
428
+ const latestCommitHash = result.success ? result.output.trim() : 'unknown';
429
+ // Get repository statistics
430
+ result = await executeShell(`cd ${repoPath} && sudo git rev-list --count '${safeResolvedRef}'`);
431
+ const totalCommits = result.success ? parseInt(result.output.trim()) : 0;
432
+ logger.success('Repository history fetched successfully', {
433
+ repoName,
434
+ branchName: resolvedBranch.displayBranch,
435
+ reflogEntries: parsedReflog.length,
436
+ commits: parsedCommits.length
437
+ });
438
+ return {
439
+ repoName,
440
+ currentBranch,
441
+ latestCommitHash,
442
+ totalCommits,
443
+ reflog: parsedReflog,
444
+ commits: parsedCommits,
445
+ summary: {
446
+ totalReflogEntries: parsedReflog.length,
447
+ totalCommitsShown: parsedCommits.length,
448
+ actionTypes: {
449
+ commits: parsedReflog.filter((e) => e.action === 'commit').length,
450
+ pulls: parsedReflog.filter((e) => e.action === 'pull').length,
451
+ merges: parsedReflog.filter((e) => e.action === 'merge').length,
452
+ checkouts: parsedReflog.filter((e) => e.action === 'checkout').length,
453
+ resets: parsedReflog.filter((e) => e.action === 'reset').length,
454
+ rebases: parsedReflog.filter((e) => e.action === 'rebase').length
455
+ }
456
+ }
457
+ };
458
+ }
459
+ finally {
460
+ }
461
+ }
462
+ /**
463
+ * Commit local changes
464
+ */
465
+ async commitLocalChanges(repoName, remoteBranchName, commitMessage, userInfo, socket, userId) {
466
+ const repoPath = `${tool_server_1.folderPath}/${repoName}`;
467
+ let commitMsg = commitMessage;
468
+ if (!commitMessage) {
469
+ commitMsg = 'Auto-commit by AI-Playgrounds Bot';
470
+ }
471
+ try {
472
+ logger.info('Starting local commit operation', { repoName, remoteBranchName, commitMessage });
473
+ // Escape commit message for shell safety
474
+ const commitAuthorName = (userInfo === null || userInfo === void 0 ? void 0 : userInfo.name) || 'Phantom';
475
+ const commitAuthorEmail = (userInfo === null || userInfo === void 0 ? void 0 : userInfo.email) || 'noreply@ai-playgrounds.com';
476
+ const escapedCommitMsg = commitMsg.replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$');
477
+ const escapedAuthorName = commitAuthorName.replace(/"/g, '\\"');
478
+ const escapedAuthorEmail = commitAuthorEmail.replace(/"/g, '\\"');
479
+ // Combine all commands into a single SSH call
480
+ const combinedCommand = `
481
+ cd ${repoPath} &&
482
+ sudo git config user.name "${escapedAuthorName}" &&
483
+ sudo git config user.email "${escapedAuthorEmail}" &&
484
+ STATUS=$(sudo git status --porcelain) &&
485
+ if [ -z "$STATUS" ]; then
486
+ echo "ERROR:NO_CHANGES";
487
+ exit 1;
488
+ fi &&
489
+ CHANGED_FILES=$(echo "$STATUS" | wc -l) &&
490
+ sudo git add . &&
491
+ sudo git commit -m "${escapedCommitMsg}" &&
492
+ COMMIT_HASH=$(sudo git rev-parse HEAD) &&
493
+ SHORT_HASH=$(sudo git rev-parse --short HEAD) &&
494
+ echo "CHANGED_FILES:$CHANGED_FILES" &&
495
+ echo "COMMIT_HASH:$COMMIT_HASH" &&
496
+ echo "SHORT_HASH:$SHORT_HASH"
497
+ `.replace(/\n\s+/g, ' ');
498
+ const result = await executeShell(combinedCommand);
499
+ if (!result.success) {
500
+ if (result.output && result.output.includes('ERROR:NO_CHANGES')) {
501
+ throw new Error('No changes to commit');
502
+ }
503
+ throw new Error('Failed to commit local changes');
504
+ }
505
+ // Parse output
506
+ const output = result.output;
507
+ const changedFilesMatch = output.match(/CHANGED_FILES:(\d+)/);
508
+ const commitHashMatch = output.match(/COMMIT_HASH:([a-f0-9]+)/);
509
+ const shortHashMatch = output.match(/SHORT_HASH:([a-f0-9]+)/);
510
+ const changedFiles = changedFilesMatch ? parseInt(changedFilesMatch[1]) : 0;
511
+ const commitHash = commitHashMatch ? commitHashMatch[1].trim() : 'unknown';
512
+ const shortHash = shortHashMatch ? shortHashMatch[1].trim() : commitHash.substring(0, 7);
513
+ logger.success('Local commit completed successfully', { repoName, commitHash, changedFiles });
514
+ const resultData = {
515
+ repoName,
516
+ branchName: remoteBranchName,
517
+ commitMessage,
518
+ commitHash,
519
+ shortHash,
520
+ changedFiles,
521
+ author: commitAuthorName,
522
+ email: commitAuthorEmail,
523
+ committedAt: new Date().toISOString()
524
+ };
525
+ // Emit socket event to refresh repo on client (only when called by AI agent)
526
+ if (socket) {
527
+ socket.emit('refresh_repo', { repoName });
528
+ }
529
+ // Update database asynchronously (non-blocking)
530
+ // if (userId) {
531
+ // (async () => {
532
+ // try {
533
+ // const dbService = await getDBService();
534
+ // const dbName = ds.UserInfo.get(userId)?.dbName;
535
+ // if (!dbName) throw new Error('User dbName not found');
536
+ // const taskRepository = dbService.getRepository(dbName, CollectionNames.TASKS);
537
+ // const task = await taskRepository.findOne();
538
+ // let gitChanges = (task?.metadata?.github) || {
539
+ // totalAdditions: 0,
540
+ // totalDeletions: 0,
541
+ // totalChangedFiles: 0,
542
+ // pullRequests: [],
543
+ // commits: []
544
+ // };
545
+ // const commitObj = {
546
+ // id: shortHash,
547
+ // sha: commitHash,
548
+ // message: commitMessage,
549
+ // author: commitAuthorEmail,
550
+ // authorName: commitAuthorName,
551
+ // timestamp: resultData.committedAt,
552
+ // url: undefined,
553
+ // additions: changedFiles,
554
+ // deletions: 0,
555
+ // changedFiles: changedFiles,
556
+ // files: []
557
+ // };
558
+ // gitChanges.commits.push(commitObj);
559
+ // gitChanges.totalChangedFiles += changedFiles;
560
+ // await taskRepository.updateOne(, { $set: { 'metadata.github': gitChanges } });
561
+ // } catch (err) {
562
+ // logger.error('Failed to update task metadata.github for commit', { err });
563
+ // }
564
+ // })();
565
+ // }
566
+ return resultData;
567
+ }
568
+ finally {
569
+ }
570
+ }
571
+ /**
572
+ * Create pull request
573
+ */
574
+ async createPullRequest(repoName, targetBranch, title, body, socket) {
575
+ const repoPath = `${tool_server_1.folderPath}/${repoName}`;
576
+ const installationToken = await fetchInstallationToken();
577
+ const gitOrgName = await getGithubOrganizationName();
578
+ try {
579
+ logger.info('Creating pull request', { repoName, targetBranch });
580
+ // Combine commands into a single SSH call, with remote URL set/unset
581
+ const combinedCommand = `
582
+ cd ${repoPath} &&
583
+ sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
584
+ HEAD_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
585
+ sudo git fetch origin ${targetBranch} 2>/dev/null &&
586
+ COMMITS_AHEAD=$(sudo git rev-list --count origin/${targetBranch}..$HEAD_BRANCH 2>/dev/null || echo 0) &&
587
+ if [ "$COMMITS_AHEAD" = "0" ]; then
588
+ echo "ERROR:NO_COMMITS:$HEAD_BRANCH";
589
+ sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git;
590
+ exit 1;
591
+ fi &&
592
+ echo "HEAD_BRANCH:$HEAD_BRANCH" &&
593
+ echo "COMMITS_AHEAD:$COMMITS_AHEAD" &&
594
+ sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git
595
+ `.replace(/\n\s+/g, ' ');
596
+ let result = await executeShell(combinedCommand);
597
+ const output = result.output || '';
598
+ const headBranchMatch = output.match(/HEAD_BRANCH:([^\n]+)/);
599
+ const commitsAheadMatch = output.match(/COMMITS_AHEAD:(\d+)/);
600
+ const errorMatch = output.match(/ERROR:NO_COMMITS:([^\n]+)/);
601
+ let headBranch = headBranchMatch ? headBranchMatch[1].trim() : 'unknown';
602
+ const commitsAhead = commitsAheadMatch ? parseInt(commitsAheadMatch[1]) : 0;
603
+ if (!result.success && errorMatch) {
604
+ headBranch = errorMatch[1].trim();
605
+ }
606
+ const { Octokit } = require('@octokit/rest');
607
+ const octokit = new Octokit({
608
+ auth: installationToken
609
+ });
610
+ // Always check for existing PR first even if there are no new commits
611
+ try {
612
+ const existingPRs = await octokit.pulls.list({
613
+ owner: gitOrgName,
614
+ repo: repoName,
615
+ head: `${gitOrgName}:${headBranch}`,
616
+ base: targetBranch,
617
+ state: 'open'
618
+ });
619
+ if (existingPRs.data.length > 0) {
620
+ const existingPR = existingPRs.data[0];
621
+ logger.info('Pull request already exists', {
622
+ repoName,
623
+ prNumber: existingPR.number,
624
+ prUrl: existingPR.html_url
625
+ });
626
+ const resultData = {
627
+ repoName,
628
+ headBranch,
629
+ targetBranch,
630
+ prNumber: existingPR.number,
631
+ prUrl: existingPR.html_url,
632
+ prTitle: existingPR.title,
633
+ commitsAhead,
634
+ createdAt: existingPR.created_at,
635
+ alreadyExists: true
636
+ };
637
+ // Emit socket event to refresh PR status on client
638
+ if (socket) {
639
+ socket.emit('refresh_pr_status', { repoName, prNumber: existingPR.number });
640
+ }
641
+ // the socket server our main server will automatically update the pr metadata in db when i recvs the result of the given tool.
642
+ // this.updatePRMetadata(userId, resultData);
643
+ return resultData;
644
+ }
645
+ }
646
+ catch (fetchError) {
647
+ logger.error('Failed to check for existing PR', fetchError);
648
+ }
649
+ // No existing PR found - check if we have commits to create one
650
+ if (!result.success) {
651
+ if (errorMatch) {
652
+ throw new Error(`No commits to create PR. Branch '${headBranch}' is not ahead of '${targetBranch}'`);
653
+ }
654
+ throw new Error('Failed to prepare pull request');
655
+ }
656
+ // Create new PR
657
+ const prTitle = title || `Merge ${headBranch} into ${targetBranch}`;
658
+ const prBody = body || `This pull request merges changes from ${headBranch} into ${targetBranch}.\n\nCreated by AI-Playgrounds`;
659
+ try {
660
+ const prResponse = await octokit.pulls.create({
661
+ owner: gitOrgName,
662
+ repo: repoName,
663
+ title: prTitle,
664
+ body: prBody,
665
+ head: headBranch,
666
+ base: targetBranch
667
+ });
668
+ const prUrl = prResponse.data.html_url;
669
+ const prNumber = prResponse.data.number;
670
+ logger.success('Pull request created successfully', {
671
+ repoName,
672
+ prNumber,
673
+ prUrl
674
+ });
675
+ // taskInfo.status = TaskStatus.Completed;
676
+ // taskInfo.nonRunningSince = new Date();
677
+ // //updating the task status in the db
678
+ // let dbService = await getDBService();
679
+ // let taskHanlder = dbService.getRepository<Task>(ds.UserInfo.get(userId as any)?.dbName, CollectionNames.TASKS);
680
+ // taskHanlder.updateOne({
681
+ // taskId: taskId
682
+ // },
683
+ // {
684
+ // "$set": {
685
+ // status: TaskStatus.Completed
686
+ // }
687
+ // });
688
+ const resultData = {
689
+ repoName,
690
+ headBranch,
691
+ targetBranch,
692
+ prNumber,
693
+ prUrl,
694
+ prTitle,
695
+ commitsAhead,
696
+ createdAt: new Date().toISOString()
697
+ };
698
+ // // Emit socket event to refresh PR status on client
699
+ // if (socket) {
700
+ // socket.emit('refresh_pr_status', { repoName, prNumber });
701
+ // }
702
+ // // Update database asynchronously (non-blocking)
703
+ // this.updatePRMetadata(userId, resultData);
704
+ return resultData;
705
+ }
706
+ catch (prError) {
707
+ // Check if PR already exists
708
+ if (prError.status === 422) {
709
+ // Fetch existing PR
710
+ try {
711
+ const existingPRs = await octokit.pulls.list({
712
+ owner: gitOrgName,
713
+ repo: repoName,
714
+ head: `${gitOrgName}:${headBranch}`,
715
+ base: targetBranch,
716
+ state: 'open'
717
+ });
718
+ if (existingPRs.data.length > 0) {
719
+ const existingPR = existingPRs.data[0];
720
+ logger.info('Pull request already exists', {
721
+ repoName,
722
+ prNumber: existingPR.number,
723
+ prUrl: existingPR.html_url
724
+ });
725
+ const resultData = {
726
+ repoName,
727
+ headBranch,
728
+ targetBranch,
729
+ prNumber: existingPR.number,
730
+ prUrl: existingPR.html_url,
731
+ prTitle: existingPR.title,
732
+ commitsAhead,
733
+ createdAt: existingPR.created_at,
734
+ alreadyExists: true
735
+ };
736
+ // Emit socket event to refresh PR status on client
737
+ if (socket) {
738
+ socket.emit('refresh_pr_status', { repoName, prNumber: existingPR.number });
739
+ }
740
+ // Update database asynchronously (non-blocking)
741
+ // this.updatePRMetadata(userId, resultData);
742
+ return resultData;
743
+ }
744
+ }
745
+ catch (fetchError) {
746
+ logger.error('Failed to fetch existing PR', fetchError);
747
+ }
748
+ }
749
+ throw prError;
750
+ }
751
+ }
752
+ finally {
753
+ }
754
+ }
755
+ /**
756
+ * Check if pull request exists
757
+ */
758
+ async mergeBranchIntoBranch(repoName, sourceBranch, targetBranch, pushToRemote = false, socket) {
759
+ const repoPath = `${tool_server_1.folderPath}/${repoName}`;
760
+ const installationToken = await fetchInstallationToken();
761
+ const gitOrgName = await getGithubOrganizationName();
762
+ try {
763
+ const trimmedSourceBranch = (sourceBranch || '').trim();
764
+ const trimmedTargetBranch = (targetBranch || '').trim();
765
+ if (!trimmedSourceBranch || !trimmedTargetBranch) {
766
+ throw new Error('Both sourceBranch and targetBranch are required');
767
+ }
768
+ if (trimmedSourceBranch === trimmedTargetBranch) {
769
+ throw new Error('sourceBranch and targetBranch cannot be the same');
770
+ }
771
+ logger.info('Starting branch merge operation', {
772
+ repoName,
773
+ sourceBranch: trimmedSourceBranch,
774
+ targetBranch: trimmedTargetBranch,
775
+ pushToRemote
776
+ });
777
+ let result = await executeShell(`cd ${repoPath} && sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git`);
778
+ if (!result.success) {
779
+ throw new Error(`Failed to set authenticated remote URL: ${result.error || result.output || 'Unknown error'}`);
780
+ }
781
+ const fetchResult = await executeShell(`cd ${repoPath} && sudo git fetch origin --prune`);
782
+ if (!fetchResult.success) {
783
+ throw new Error(`Failed to fetch remote branches: ${fetchResult.error || fetchResult.output || 'Unknown error'}`);
784
+ }
785
+ const resolvedSource = await this.resolveBranchReference(repoPath, repoName, trimmedSourceBranch);
786
+ const resolvedTarget = await this.resolveBranchReference(repoPath, repoName, trimmedTargetBranch);
787
+ const targetLocalBranch = resolvedTarget.displayBranch;
788
+ const safeTargetLocalBranch = this.escapeSingleQuotedShell(targetLocalBranch);
789
+ const safeSourceRef = this.escapeSingleQuotedShell(resolvedSource.resolvedRef);
790
+ const prepareBranchCommand = `
791
+ cd ${repoPath} &&
792
+ TARGET_LOCAL='${safeTargetLocalBranch}' &&
793
+ if sudo git show-ref --verify --quiet "refs/heads/$TARGET_LOCAL"; then
794
+ sudo git checkout "$TARGET_LOCAL";
795
+ elif sudo git show-ref --verify --quiet "refs/remotes/origin/$TARGET_LOCAL"; then
796
+ sudo git checkout -b "$TARGET_LOCAL" "origin/$TARGET_LOCAL";
797
+ else
798
+ echo "ERROR:TARGET_BRANCH_NOT_CHECKOUTABLE";
799
+ exit 1;
800
+ fi &&
801
+ if sudo git show-ref --verify --quiet "refs/remotes/origin/$TARGET_LOCAL"; then
802
+ sudo git pull --ff-only origin "$TARGET_LOCAL" 2>/dev/null || true;
803
+ fi
804
+ `.replace(/\n\s+/g, ' ');
805
+ result = await executeShell(prepareBranchCommand);
806
+ if (!result.success) {
807
+ throw new Error(`Failed to prepare target branch for merge: ${result.error || result.output || 'Unknown error'}`);
808
+ }
809
+ const mergeCommand = `cd ${repoPath} && SOURCE_REF='${safeSourceRef}' && sudo git merge --no-ff --no-edit "$SOURCE_REF"`;
810
+ const mergeResult = await executeShell(mergeCommand);
811
+ if (!mergeResult.success) {
812
+ const conflictResult = await executeShell(`cd ${repoPath} && sudo git diff --name-only --diff-filter=U`);
813
+ const conflictFiles = (conflictResult.output || '')
814
+ .split('\n')
815
+ .map((line) => line.trim())
816
+ .filter((line) => line.length > 0);
817
+ if (conflictFiles.length > 0) {
818
+ logger.warn('Merge completed with conflicts', {
819
+ repoName,
820
+ sourceBranch: resolvedSource.displayBranch,
821
+ targetBranch: targetLocalBranch,
822
+ conflictFiles
823
+ });
824
+ return {
825
+ repoName,
826
+ sourceBranch: resolvedSource.displayBranch,
827
+ targetBranch: targetLocalBranch,
828
+ merged: false,
829
+ hasConflicts: true,
830
+ conflictFiles,
831
+ currentBranch: targetLocalBranch,
832
+ pushedToRemote: false,
833
+ mergedAt: new Date().toISOString(),
834
+ message: `Merge has conflicts. Resolve conflicts in target branch '${targetLocalBranch}'.`
835
+ };
836
+ }
837
+ throw new Error(`Failed to merge branches: ${mergeResult.error || mergeResult.output || 'Unknown error'}`);
838
+ }
839
+ const summaryResult = await executeShell(`cd ${repoPath} && COMMIT_HASH=$(sudo git rev-parse HEAD) && CURRENT_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) && echo "COMMIT_HASH:$COMMIT_HASH" && echo "CURRENT_BRANCH:$CURRENT_BRANCH"`);
840
+ if (!summaryResult.success) {
841
+ throw new Error(`Merge succeeded but failed to fetch merge summary: ${summaryResult.error || summaryResult.output || 'Unknown error'}`);
842
+ }
843
+ const summaryOutput = summaryResult.output || '';
844
+ const commitHashMatch = summaryOutput.match(/COMMIT_HASH:([^\n]+)/);
845
+ const currentBranchMatch = summaryOutput.match(/CURRENT_BRANCH:([^\n]+)/);
846
+ const commitHash = commitHashMatch ? commitHashMatch[1].trim() : '';
847
+ const currentBranch = currentBranchMatch ? currentBranchMatch[1].trim() : targetLocalBranch;
848
+ let pushedToRemote = false;
849
+ if (pushToRemote) {
850
+ const pushResult = await executeShell(`cd ${repoPath} && sudo git push origin '${safeTargetLocalBranch}'`);
851
+ if (!pushResult.success) {
852
+ throw new Error(`Merge succeeded but push failed: ${pushResult.error || pushResult.output || 'Unknown error'}`);
853
+ }
854
+ pushedToRemote = true;
855
+ }
856
+ logger.success('Branch merge completed successfully', {
857
+ repoName,
858
+ sourceBranch: resolvedSource.displayBranch,
859
+ targetBranch: targetLocalBranch,
860
+ commitHash,
861
+ pushedToRemote
862
+ });
863
+ if (socket) {
864
+ socket.emit('refresh_repo', { repoName });
865
+ }
866
+ return {
867
+ repoName,
868
+ sourceBranch: resolvedSource.displayBranch,
869
+ targetBranch: targetLocalBranch,
870
+ merged: true,
871
+ hasConflicts: false,
872
+ conflictFiles: [],
873
+ currentBranch,
874
+ commitHash,
875
+ pushedToRemote,
876
+ mergedAt: new Date().toISOString(),
877
+ message: pushedToRemote
878
+ ? `Merged '${resolvedSource.displayBranch}' into '${targetLocalBranch}' and pushed to remote.`
879
+ : `Merged '${resolvedSource.displayBranch}' into '${targetLocalBranch}' locally.`
880
+ };
881
+ }
882
+ finally {
883
+ try {
884
+ await executeShell(`cd ${repoPath} && sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git`);
885
+ }
886
+ catch (restoreError) {
887
+ logger.error('Failed to restore unauthenticated remote URL after merge operation', {
888
+ repoName,
889
+ error: restoreError instanceof Error ? restoreError.message : String(restoreError)
890
+ });
891
+ }
892
+ }
893
+ }
894
+ async checkPullRequestExists(repoName, targetBranch) {
895
+ const repoPath = `${tool_server_1.folderPath}/${repoName}`;
896
+ const installationToken = await fetchInstallationToken();
897
+ const gitOrgName = await getGithubOrganizationName();
898
+ try {
899
+ logger.info('Checking if PR exists', { repoName, targetBranch });
900
+ // Get current branch and commits ahead
901
+ const combinedCommand = `
902
+ cd ${repoPath} &&
903
+ sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
904
+ HEAD_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
905
+ sudo git fetch origin ${targetBranch} &&
906
+ COMMITS_AHEAD=$(sudo git rev-list --count origin/${targetBranch}..$HEAD_BRANCH 2>/dev/null || echo 0) &&
907
+ sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
908
+ echo "HEAD_BRANCH:$HEAD_BRANCH" &&
909
+ echo "COMMITS_AHEAD:$COMMITS_AHEAD"
910
+ `.replace(/\n\s+/g, ' ');
911
+ const result = await executeShell(combinedCommand);
912
+ // Check for command execution failure
913
+ if (!result.success) {
914
+ logger.error('Failed to check PR status', {
915
+ repoName,
916
+ error: result.error,
917
+ code: result.code,
918
+ output: result.output
919
+ });
920
+ throw new Error(`Failed to check PR status: ${result.error}`);
921
+ }
922
+ const output = result.output || '';
923
+ const headBranchMatch = output.match(/HEAD_BRANCH:([^\n]+)/);
924
+ const commitsAheadMatch = output.match(/COMMITS_AHEAD:(\d+)/);
925
+ const headBranch = headBranchMatch ? headBranchMatch[1].trim() : 'unknown';
926
+ const commitsAhead = commitsAheadMatch ? parseInt(commitsAheadMatch[1]) : 0;
927
+ logger.info('Branch info retrieved', { headBranch, commitsAhead });
928
+ // Check for existing PR
929
+ const { Octokit } = require('@octokit/rest');
930
+ const octokit = new Octokit({
931
+ auth: installationToken
932
+ });
933
+ const existingPRs = await octokit.pulls.list({
934
+ owner: gitOrgName,
935
+ repo: repoName,
936
+ head: `${gitOrgName}:${headBranch}`,
937
+ base: targetBranch,
938
+ state: 'open'
939
+ });
940
+ if (existingPRs.data.length > 0) {
941
+ const existingPR = existingPRs.data[0];
942
+ logger.info('Pull request exists', {
943
+ repoName,
944
+ prNumber: existingPR.number,
945
+ prUrl: existingPR.html_url
946
+ });
947
+ return {
948
+ repoName,
949
+ headBranch,
950
+ targetBranch,
951
+ prExists: true,
952
+ prNumber: existingPR.number,
953
+ prUrl: existingPR.html_url,
954
+ prTitle: existingPR.title,
955
+ commitsAhead,
956
+ createdAt: existingPR.created_at
957
+ };
958
+ }
959
+ else {
960
+ logger.info('No pull request found', { repoName, headBranch, targetBranch });
961
+ return {
962
+ repoName,
963
+ headBranch,
964
+ targetBranch,
965
+ prExists: false,
966
+ commitsAhead
967
+ };
968
+ }
969
+ }
970
+ finally {
971
+ }
972
+ }
973
+ /**
974
+ * Get commit list with commit IDs for a branch
975
+ */
976
+ async getCommitList(repoName, branchName, limit = 100) {
977
+ const repoPath = `${tool_server_1.folderPath}/${repoName}`;
978
+ try {
979
+ logger.info('Fetching commit list', { repoName, branchName, limit });
980
+ const safeLimit = Number.isFinite(limit) ? Math.min(Math.max(limit, 1), 500) : 100;
981
+ let targetBranch = (branchName || '').trim();
982
+ if (!targetBranch) {
983
+ const branchResult = await executeShell(`cd ${repoPath} && sudo git rev-parse --abbrev-ref HEAD`);
984
+ if (!branchResult.success) {
985
+ throw new Error(`Failed to resolve current branch: ${branchResult.error || branchResult.output || 'Unknown error'}`);
986
+ }
987
+ targetBranch = branchResult.output.trim();
988
+ }
989
+ const resolvedBranch = await this.resolveBranchReference(repoPath, repoName, targetBranch);
990
+ const safeResolvedRef = this.escapeSingleQuotedShell(resolvedBranch.resolvedRef);
991
+ const logResult = await executeShell(`cd ${repoPath} && sudo git log '${safeResolvedRef}' -${safeLimit} --pretty=format:"%H%x1f%h%x1f%an%x1f%ae%x1f%ad%x1f%s" --date=iso`);
992
+ if (!logResult.success) {
993
+ throw new Error(`Failed to fetch commit list: ${logResult.error || logResult.output || 'Unknown error'}`);
994
+ }
995
+ const commitLines = (logResult.output || '').trim()
996
+ ? logResult.output.trim().split('\n').filter((line) => line.trim())
997
+ : [];
998
+ const commits = commitLines
999
+ .map((line) => {
1000
+ const [fullHash, hash, author, email, date, ...messageParts] = line.split('\x1f');
1001
+ return {
1002
+ hash: (hash || '').trim(),
1003
+ fullHash: (fullHash || '').trim(),
1004
+ author: (author || '').trim(),
1005
+ email: (email || '').trim(),
1006
+ date: (date || '').trim(),
1007
+ message: messageParts.join('\x1f').trim()
1008
+ };
1009
+ })
1010
+ .filter((commit) => commit.fullHash);
1011
+ logger.success('Commit list fetched successfully', {
1012
+ repoName,
1013
+ branchName: resolvedBranch.displayBranch,
1014
+ commits: commits.length
1015
+ });
1016
+ return {
1017
+ repoName,
1018
+ branchName: resolvedBranch.displayBranch,
1019
+ totalCommits: commits.length,
1020
+ commits
1021
+ };
1022
+ }
1023
+ finally {
1024
+ }
1025
+ }
1026
+ /**
1027
+ * Get latest commit diff for a given branch
1028
+ */
1029
+ async getLatestCommitDiff(repoName, branchName) {
1030
+ const repoPath = `${tool_server_1.folderPath}/${repoName}`;
1031
+ try {
1032
+ logger.info('Fetching latest commit diff', { repoName, branchName });
1033
+ let targetBranch = (branchName || '').trim();
1034
+ if (!targetBranch) {
1035
+ const branchResult = await executeShell(`cd ${repoPath} && sudo git rev-parse --abbrev-ref HEAD`);
1036
+ if (!branchResult.success) {
1037
+ throw new Error(`Failed to resolve current branch: ${branchResult.error || branchResult.output || 'Unknown error'}`);
1038
+ }
1039
+ targetBranch = branchResult.output.trim();
1040
+ }
1041
+ const resolvedBranch = await this.resolveBranchReference(repoPath, repoName, targetBranch);
1042
+ const safeResolvedRef = this.escapeSingleQuotedShell(resolvedBranch.resolvedRef);
1043
+ const commitInfoResult = await executeShell(`cd ${repoPath} && sudo git show --no-patch --pretty=format:"%H%x1f%h%x1f%an%x1f%ae%x1f%ad%x1f%s" --date=iso '${safeResolvedRef}'`);
1044
+ if (!commitInfoResult.success) {
1045
+ throw new Error(`Failed to fetch latest commit information: ${commitInfoResult.error || commitInfoResult.output || 'Unknown error'}`);
1046
+ }
1047
+ const commitInfo = (commitInfoResult.output || '').trim();
1048
+ if (!commitInfo) {
1049
+ throw new Error(`No commits found on branch '${resolvedBranch.displayBranch}'`);
1050
+ }
1051
+ const [fullHash, hash, author, email, date, ...messageParts] = commitInfo.split('\x1f');
1052
+ const commitMessage = messageParts.join('\x1f').trim();
1053
+ const safeCommitHash = this.escapeSingleQuotedShell((fullHash || '').trim());
1054
+ const diffResult = await executeShell(`cd ${repoPath} && sudo git show --pretty=format:"" '${safeCommitHash}'`);
1055
+ if (!diffResult.success) {
1056
+ throw new Error(`Failed to fetch diff for commit '${hash}': ${diffResult.error || diffResult.output || 'Unknown error'}`);
1057
+ }
1058
+ const statsResult = await executeShell(`cd ${repoPath} && sudo git show --numstat --pretty="" '${safeCommitHash}'`);
1059
+ if (!statsResult.success) {
1060
+ throw new Error(`Failed to fetch commit stats for '${hash}': ${statsResult.error || statsResult.output || 'Unknown error'}`);
1061
+ }
1062
+ const statLines = (statsResult.output || '').trim()
1063
+ ? statsResult.output.trim().split('\n').filter((line) => line.trim())
1064
+ : [];
1065
+ let totalAdditions = 0;
1066
+ let totalDeletions = 0;
1067
+ for (const line of statLines) {
1068
+ const [additionsRaw, deletionsRaw] = line.split('\t');
1069
+ const additions = additionsRaw === '-' ? 0 : parseInt(additionsRaw, 10) || 0;
1070
+ const deletions = deletionsRaw === '-' ? 0 : parseInt(deletionsRaw, 10) || 0;
1071
+ totalAdditions += additions;
1072
+ totalDeletions += deletions;
1073
+ }
1074
+ logger.success('Latest commit diff fetched successfully', {
1075
+ repoName,
1076
+ branchName: resolvedBranch.displayBranch,
1077
+ commitHash: hash,
1078
+ filesChanged: statLines.length
1079
+ });
1080
+ return {
1081
+ repoName,
1082
+ branchName: resolvedBranch.displayBranch,
1083
+ commit: {
1084
+ hash: (hash || '').trim(),
1085
+ fullHash: (fullHash || '').trim(),
1086
+ author: (author || '').trim(),
1087
+ email: (email || '').trim(),
1088
+ date: (date || '').trim(),
1089
+ message: commitMessage
1090
+ },
1091
+ diff: diffResult.output || '',
1092
+ summary: {
1093
+ filesChanged: statLines.length,
1094
+ totalAdditions,
1095
+ totalDeletions,
1096
+ totalChanges: totalAdditions + totalDeletions
1097
+ }
1098
+ };
1099
+ }
1100
+ finally {
1101
+ }
1102
+ }
1103
+ /**
1104
+ * Get diff for a specific commit hash
1105
+ */
1106
+ async getCommitDiffByHash(repoName, commitHash) {
1107
+ const repoPath = `${tool_server_1.folderPath}/${repoName}`;
1108
+ try {
1109
+ const requestedCommit = (commitHash || '').trim();
1110
+ if (!requestedCommit) {
1111
+ throw new Error('commitHash is required');
1112
+ }
1113
+ logger.info('Fetching commit diff by hash', { repoName, commitHash: requestedCommit });
1114
+ const sanitizedCommitHash = requestedCommit.replace(/'/g, `'\\''`);
1115
+ const commitCheck = await executeShell(`cd ${repoPath} && sudo git rev-parse --verify '${sanitizedCommitHash}^{commit}' >/dev/null 2>&1 && echo "COMMIT_OK"`);
1116
+ if (!commitCheck.success || !(commitCheck.output || '').includes('COMMIT_OK')) {
1117
+ throw new Error(`Commit '${requestedCommit}' not found in repository '${repoName}'`);
1118
+ }
1119
+ const commitInfoResult = await executeShell(`cd ${repoPath} && sudo git show --no-patch --pretty=format:"%H%x1f%h%x1f%an%x1f%ae%x1f%ad%x1f%s" --date=iso '${sanitizedCommitHash}'`);
1120
+ if (!commitInfoResult.success) {
1121
+ throw new Error(`Failed to fetch commit information: ${commitInfoResult.error || commitInfoResult.output || 'Unknown error'}`);
1122
+ }
1123
+ const commitInfo = (commitInfoResult.output || '').trim();
1124
+ if (!commitInfo) {
1125
+ throw new Error(`No commit metadata found for '${requestedCommit}'`);
1126
+ }
1127
+ const [fullHash, hash, author, email, date, ...messageParts] = commitInfo.split('\x1f');
1128
+ const commitMessage = messageParts.join('\x1f').trim();
1129
+ const diffResult = await executeShell(`cd ${repoPath} && sudo git show --pretty=format:"" '${sanitizedCommitHash}'`);
1130
+ if (!diffResult.success) {
1131
+ throw new Error(`Failed to fetch diff for commit '${requestedCommit}': ${diffResult.error || diffResult.output || 'Unknown error'}`);
1132
+ }
1133
+ const statsResult = await executeShell(`cd ${repoPath} && sudo git show --numstat --pretty="" '${sanitizedCommitHash}'`);
1134
+ if (!statsResult.success) {
1135
+ throw new Error(`Failed to fetch commit stats for '${requestedCommit}': ${statsResult.error || statsResult.output || 'Unknown error'}`);
1136
+ }
1137
+ const statLines = (statsResult.output || '').trim()
1138
+ ? statsResult.output.trim().split('\n').filter((line) => line.trim())
1139
+ : [];
1140
+ let totalAdditions = 0;
1141
+ let totalDeletions = 0;
1142
+ for (const line of statLines) {
1143
+ const [additionsRaw, deletionsRaw] = line.split('\t');
1144
+ const additions = additionsRaw === '-' ? 0 : parseInt(additionsRaw, 10) || 0;
1145
+ const deletions = deletionsRaw === '-' ? 0 : parseInt(deletionsRaw, 10) || 0;
1146
+ totalAdditions += additions;
1147
+ totalDeletions += deletions;
1148
+ }
1149
+ logger.success('Commit diff fetched successfully', {
1150
+ repoName,
1151
+ commitHash: hash,
1152
+ filesChanged: statLines.length
1153
+ });
1154
+ return {
1155
+ repoName,
1156
+ commit: {
1157
+ hash: (hash || '').trim(),
1158
+ fullHash: (fullHash || '').trim(),
1159
+ author: (author || '').trim(),
1160
+ email: (email || '').trim(),
1161
+ date: (date || '').trim(),
1162
+ message: commitMessage
1163
+ },
1164
+ diff: diffResult.output || '',
1165
+ summary: {
1166
+ filesChanged: statLines.length,
1167
+ totalAdditions,
1168
+ totalDeletions,
1169
+ totalChanges: totalAdditions + totalDeletions
1170
+ }
1171
+ };
1172
+ }
1173
+ finally {
1174
+ }
1175
+ }
1176
+ /**
1177
+ * Get commit details
1178
+ */
1179
+ async getCommitDetails(repoName, commitHash) {
1180
+ const repoPath = `${tool_server_1.folderPath}/${repoName}`;
1181
+ try {
1182
+ logger.info('Fetching commit details', { repoName, commitHash });
1183
+ // Get commit information and file changes in a single SSH call
1184
+ const combinedCommand = `
1185
+ cd ${repoPath} &&
1186
+ COMMIT_INFO=$(sudo git show --no-patch --pretty=format:"%H|%h|%an|%ae|%ad|%s" --date=iso ${commitHash}) &&
1187
+ FILE_STATS=$(sudo git show --stat --pretty="" --numstat ${commitHash}) &&
1188
+ echo "COMMIT_INFO:$COMMIT_INFO" &&
1189
+ echo "FILE_STATS_START" &&
1190
+ echo "$FILE_STATS" &&
1191
+ echo "FILE_STATS_END"
1192
+ `.replace(/\n\s+/g, ' ');
1193
+ const result = await executeShell(combinedCommand);
1194
+ if (!result.success) {
1195
+ throw new Error('Failed to fetch commit details');
1196
+ }
1197
+ // Parse commit info
1198
+ const output = result.output;
1199
+ const commitInfoMatch = output.match(/COMMIT_INFO:([^\n]+)/);
1200
+ if (!commitInfoMatch) {
1201
+ throw new Error('Failed to parse commit information');
1202
+ }
1203
+ const [fullHash, shortHash, author, email, date, message] = commitInfoMatch[1].split('|');
1204
+ // Parse file statistics
1205
+ const fileStatsMatch = output.match(/FILE_STATS_START\n([\s\S]*?)FILE_STATS_END/);
1206
+ const fileStatsRaw = fileStatsMatch ? fileStatsMatch[1].trim() : '';
1207
+ const modifiedFiles = [];
1208
+ let totalAdditions = 0;
1209
+ let totalDeletions = 0;
1210
+ if (fileStatsRaw) {
1211
+ const lines = fileStatsRaw.split('\n').filter((line) => line.trim());
1212
+ for (const line of lines) {
1213
+ // Format: additions\tdeletions\tfilename
1214
+ const parts = line.split('\t');
1215
+ if (parts.length >= 3) {
1216
+ const additions = parts[0] === '-' ? 0 : parseInt(parts[0]) || 0;
1217
+ const deletions = parts[1] === '-' ? 0 : parseInt(parts[1]) || 0;
1218
+ const filename = parts[2];
1219
+ // Determine file status
1220
+ let status = 'modified';
1221
+ if (additions > 0 && deletions === 0) {
1222
+ status = 'added';
1223
+ }
1224
+ else if (additions === 0 && deletions > 0) {
1225
+ status = 'deleted';
1226
+ }
1227
+ else if (parts[0] === '-' && parts[1] === '-') {
1228
+ status = 'binary';
1229
+ }
1230
+ modifiedFiles.push({
1231
+ filename,
1232
+ additions,
1233
+ deletions,
1234
+ changes: additions + deletions,
1235
+ status
1236
+ });
1237
+ totalAdditions += additions;
1238
+ totalDeletions += deletions;
1239
+ }
1240
+ }
1241
+ }
1242
+ logger.success('Commit details fetched successfully', {
1243
+ repoName,
1244
+ commitHash: shortHash,
1245
+ filesModified: modifiedFiles.length
1246
+ });
1247
+ return {
1248
+ repoName,
1249
+ commit: {
1250
+ hash: shortHash,
1251
+ fullHash,
1252
+ author,
1253
+ email,
1254
+ date,
1255
+ message
1256
+ },
1257
+ files: modifiedFiles,
1258
+ summary: {
1259
+ totalFiles: modifiedFiles.length,
1260
+ totalAdditions,
1261
+ totalDeletions,
1262
+ totalChanges: totalAdditions + totalDeletions,
1263
+ filesByStatus: {
1264
+ added: modifiedFiles.filter(f => f.status === 'added').length,
1265
+ modified: modifiedFiles.filter(f => f.status === 'modified').length,
1266
+ deleted: modifiedFiles.filter(f => f.status === 'deleted').length,
1267
+ binary: modifiedFiles.filter(f => f.status === 'binary').length
1268
+ }
1269
+ }
1270
+ };
1271
+ }
1272
+ finally {
1273
+ }
1274
+ }
1275
+ // Helper function to parse conflict markers in files
1276
+ async parseConflictMarkers(filePath, repoPath) {
1277
+ const fs = await Promise.resolve().then(() => __importStar(require('fs')));
1278
+ const path = await Promise.resolve().then(() => __importStar(require('path')));
1279
+ const fullPath = path.join(repoPath, filePath);
1280
+ const content = await fs.promises.readFile(fullPath, 'utf-8');
1281
+ const lines = content.split('\n');
1282
+ const conflicts = [];
1283
+ let i = 0;
1284
+ while (i < lines.length) {
1285
+ if (lines[i].startsWith('<<<<<<<')) {
1286
+ const startLine = i;
1287
+ const currentBranch = lines[i].replace('<<<<<<< ', '').trim();
1288
+ // Find the separator
1289
+ let separatorLine = -1;
1290
+ for (let j = i + 1; j < lines.length; j++) {
1291
+ if (lines[j].startsWith('=======')) {
1292
+ separatorLine = j;
1293
+ break;
1294
+ }
1295
+ }
1296
+ // Find the end marker
1297
+ let endLine = -1;
1298
+ let incomingBranch = '';
1299
+ for (let j = separatorLine + 1; j < lines.length; j++) {
1300
+ if (lines[j].startsWith('>>>>>>>')) {
1301
+ endLine = j;
1302
+ incomingBranch = lines[j].replace('>>>>>>> ', '').trim();
1303
+ break;
1304
+ }
1305
+ }
1306
+ if (separatorLine !== -1 && endLine !== -1) {
1307
+ // Check if there's a base content (3-way merge marker |||||||)
1308
+ let baseContent;
1309
+ let baseMarkerLine = -1;
1310
+ for (let j = i + 1; j < separatorLine; j++) {
1311
+ if (lines[j].startsWith('|||||||')) {
1312
+ baseMarkerLine = j;
1313
+ break;
1314
+ }
1315
+ }
1316
+ if (baseMarkerLine !== -1) {
1317
+ // 3-way merge
1318
+ const actualCurrentContent = lines.slice(i + 1, baseMarkerLine).join('\n');
1319
+ baseContent = lines.slice(baseMarkerLine + 1, separatorLine).join('\n');
1320
+ const incomingContent = lines.slice(separatorLine + 1, endLine).join('\n');
1321
+ conflicts.push({
1322
+ startLine: startLine + 1, // 1-based line numbers
1323
+ endLine: endLine + 1,
1324
+ currentContent: actualCurrentContent,
1325
+ incomingContent: incomingContent,
1326
+ baseContent: baseContent,
1327
+ currentBranch: currentBranch,
1328
+ incomingBranch: incomingBranch
1329
+ });
1330
+ }
1331
+ else {
1332
+ // 2-way merge
1333
+ const currentContent = lines.slice(i + 1, separatorLine).join('\n');
1334
+ const incomingContent = lines.slice(separatorLine + 1, endLine).join('\n');
1335
+ conflicts.push({
1336
+ startLine: startLine + 1,
1337
+ endLine: endLine + 1,
1338
+ currentContent: currentContent,
1339
+ incomingContent: incomingContent,
1340
+ currentBranch: currentBranch,
1341
+ incomingBranch: incomingBranch
1342
+ });
1343
+ }
1344
+ i = endLine + 1;
1345
+ }
1346
+ else {
1347
+ i++;
1348
+ }
1349
+ }
1350
+ else {
1351
+ i++;
1352
+ }
1353
+ }
1354
+ return conflicts;
1355
+ }
1356
+ async getMergeConflictsImplementation(repoName, folderPath) {
1357
+ console.log('[Merge Conflicts] ===============================');
1358
+ console.log('[Merge Conflicts] Checking for conflicts in repo:', repoName);
1359
+ try {
1360
+ const fs = await Promise.resolve().then(() => __importStar(require('fs')));
1361
+ const path = await Promise.resolve().then(() => __importStar(require('path')));
1362
+ // Resolve full repo path from base folderPath and repoName
1363
+ const repoPath = path.join(folderPath, repoName);
1364
+ console.log(`[Merge Conflicts] Resolved repo path: ${repoPath}`);
1365
+ // Check if the repo path exists
1366
+ if (!fs.existsSync(repoPath)) {
1367
+ return {
1368
+ success: false,
1369
+ error: `Repository path does not exist: ${repoPath}`,
1370
+ folderPath: folderPath,
1371
+ repoName: repoName
1372
+ };
1373
+ }
1374
+ // Check if it's a git repository
1375
+ const gitPath = path.join(repoPath, '.git');
1376
+ if (!fs.existsSync(gitPath)) {
1377
+ return {
1378
+ success: false,
1379
+ error: `Not a git repository: ${repoPath}`,
1380
+ folderPath: folderPath,
1381
+ repoName: repoName
1382
+ };
1383
+ }
1384
+ const git = (0, simple_git_1.simpleGit)(repoPath);
1385
+ const status = await git.status();
1386
+ const currentBranch = status.current || 'unknown';
1387
+ console.log(`[Merge Conflicts] Current branch: ${currentBranch}`);
1388
+ console.log(`[Merge Conflicts] Conflicted files: ${status.conflicted.length}`);
1389
+ console.log(`[Merge Conflicts] Is merging: ${status.conflicted.length > 0 ? 'Yes' : 'No'}`);
1390
+ if (status.conflicted.length === 0) {
1391
+ console.log(`[Merge Conflicts] No conflicts found in repository`);
1392
+ return {
1393
+ success: true,
1394
+ hasConflicts: false,
1395
+ conflicts: [],
1396
+ message: 'No merge conflicts detected in the repository.',
1397
+ currentBranch: currentBranch,
1398
+ folderPath: folderPath,
1399
+ repoName: repoName
1400
+ };
1401
+ }
1402
+ const conflictFiles = [];
1403
+ let totalConflictsCount = 0;
1404
+ console.log(`[Merge Conflicts] Processing ${status.conflicted.length} conflicted file(s)...`);
1405
+ for (const conflictedFile of status.conflicted) {
1406
+ try {
1407
+ const fullPath = path.join(repoPath, conflictedFile);
1408
+ // Check if file exists
1409
+ if (!fs.existsSync(fullPath)) {
1410
+ console.warn(`[Merge Conflicts] File not found: ${conflictedFile}`);
1411
+ conflictFiles.push({
1412
+ file: conflictedFile,
1413
+ conflicts: [],
1414
+ fullContent: ''
1415
+ });
1416
+ continue;
1417
+ }
1418
+ const fullContent = await fs.promises.readFile(fullPath, 'utf-8');
1419
+ // Parse conflict markers
1420
+ const conflicts = await this.parseConflictMarkers(conflictedFile, repoPath);
1421
+ totalConflictsCount += conflicts.length;
1422
+ conflictFiles.push({
1423
+ file: conflictedFile,
1424
+ conflicts: conflicts,
1425
+ fullContent: fullContent
1426
+ });
1427
+ console.log(`[Merge Conflicts] File: ${conflictedFile} - ${conflicts.length} conflict block(s) found`);
1428
+ conflicts.forEach((conflict, index) => {
1429
+ console.log(`[Merge Conflicts] Conflict #${index + 1}: Lines ${conflict.startLine}-${conflict.endLine}`);
1430
+ console.log(`[Merge Conflicts] Current (${conflict.currentBranch}): ${conflict.currentContent.substring(0, 50)}...`);
1431
+ console.log(`[Merge Conflicts] Incoming (${conflict.incomingBranch}): ${conflict.incomingContent.substring(0, 50)}...`);
1432
+ });
1433
+ }
1434
+ catch (parseError) {
1435
+ console.error(`[Merge Conflicts] Error parsing ${conflictedFile}:`, parseError);
1436
+ conflictFiles.push({
1437
+ file: conflictedFile,
1438
+ conflicts: [],
1439
+ fullContent: ''
1440
+ });
1441
+ }
1442
+ }
1443
+ console.log(`[Merge Conflicts] Summary: ${conflictFiles.length} file(s) with ${totalConflictsCount} total conflict block(s)`);
1444
+ // Send response with all conflict information
1445
+ return {
1446
+ success: true,
1447
+ hasConflicts: true,
1448
+ conflicts: conflictFiles,
1449
+ message: `Found ${totalConflictsCount} conflict block(s) in ${conflictFiles.length} file(s).`,
1450
+ currentBranch: currentBranch,
1451
+ totalFiles: conflictFiles.length,
1452
+ totalConflicts: totalConflictsCount,
1453
+ folderPath: folderPath,
1454
+ repoName: repoName
1455
+ };
1456
+ }
1457
+ catch (error) {
1458
+ console.error('[Merge Conflicts] Error checking for conflicts:', error);
1459
+ return {
1460
+ success: false,
1461
+ error: error instanceof Error ? error.message : 'Unknown error occurred while checking for merge conflicts',
1462
+ folderPath: folderPath,
1463
+ repoName: repoName
1464
+ };
1465
+ }
1466
+ }
1467
+ /**
1468
+ * Helper method to get merge conflicts
1469
+ */
1470
+ async getMergeConflicts(socket, repoName) {
1471
+ return new Promise((resolve) => {
1472
+ this.getMergeConflictsImplementation(repoName, tool_server_1.folderPath).then((mergeData) => {
1473
+ resolve(mergeData);
1474
+ }).catch((error) => {
1475
+ console.error('Error in getMergeConflictsImplementation:', error);
1476
+ resolve({
1477
+ success: false,
1478
+ error: error instanceof Error ? error.message : 'Unknown error occurred while getting merge conflicts',
1479
+ folderPath: tool_server_1.folderPath,
1480
+ repoName: repoName
1481
+ });
1482
+ });
1483
+ });
1484
+ }
1485
+ }
1486
+ exports.GithubOperationsService = GithubOperationsService;
1487
+ //# sourceMappingURL=githubOperationsHanlder.js.map