deepdebug-local-agent 0.3.1 β 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/install.js +419 -0
- package/package.json +1 -1
- package/src/git/base-git-provider.js +169 -18
- package/src/git/bitbucket-provider.js +723 -0
- package/src/git/git-provider-registry.js +12 -11
- package/src/git/github-provider.js +299 -6
- package/src/server.js +587 -42
package/src/server.js
CHANGED
|
@@ -734,6 +734,71 @@ app.post("/workspace/open", async (req, res) => {
|
|
|
734
734
|
res.json({ ok: true, root: WORKSPACE_ROOT, workspaceId: wsId, meta, port });
|
|
735
735
|
});
|
|
736
736
|
|
|
737
|
+
/**
|
|
738
|
+
* POST /workspace/clone
|
|
739
|
+
* Clones a git repository into the local filesystem, then opens it as workspace.
|
|
740
|
+
* Body: { gitUrl, targetPath, workspaceId? }
|
|
741
|
+
*/
|
|
742
|
+
app.post("/workspace/clone", async (req, res) => {
|
|
743
|
+
const { gitUrl, targetPath, workspaceId } = req.body || {};
|
|
744
|
+
|
|
745
|
+
if (!gitUrl) return res.status(400).json({ ok: false, error: "gitUrl is required" });
|
|
746
|
+
if (!targetPath) return res.status(400).json({ ok: false, error: "targetPath is required" });
|
|
747
|
+
|
|
748
|
+
const absTarget = path.resolve(targetPath);
|
|
749
|
+
console.log(`π₯ Clone request: ${gitUrl} -> ${absTarget}`);
|
|
750
|
+
|
|
751
|
+
try {
|
|
752
|
+
// Ensure parent directory exists
|
|
753
|
+
const parentDir = path.dirname(absTarget);
|
|
754
|
+
await fsPromises.mkdir(parentDir, { recursive: true });
|
|
755
|
+
|
|
756
|
+
// If already cloned, do git pull instead
|
|
757
|
+
const gitDir = path.join(absTarget, '.git');
|
|
758
|
+
const alreadyCloned = await exists(gitDir);
|
|
759
|
+
|
|
760
|
+
if (alreadyCloned) {
|
|
761
|
+
console.log(`π Repo already exists at ${absTarget}, running git pull...`);
|
|
762
|
+
const { stdout } = await execAsync('git pull', { cwd: absTarget, timeout: 120000 });
|
|
763
|
+
console.log(`β
git pull: ${stdout.trim()}`);
|
|
764
|
+
} else {
|
|
765
|
+
console.log(`π½ Running: git clone "${gitUrl}" "${absTarget}"`);
|
|
766
|
+
await execAsync(`git clone "${gitUrl}" "${absTarget}"`, { timeout: 300000 });
|
|
767
|
+
console.log(`β
Clone complete: ${absTarget}`);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Open the cloned workspace
|
|
771
|
+
WORKSPACE_ROOT = absTarget;
|
|
772
|
+
const wsId = workspaceId || "default";
|
|
773
|
+
try {
|
|
774
|
+
await wsManager.open(wsId, absTarget);
|
|
775
|
+
} catch (err) {
|
|
776
|
+
console.warn(`β οΈ WorkspaceManager.open failed (non-fatal): ${err.message}`);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const meta = await detectProject(absTarget);
|
|
780
|
+
const port = await detectPort(absTarget);
|
|
781
|
+
|
|
782
|
+
res.json({
|
|
783
|
+
ok: true,
|
|
784
|
+
path: absTarget,
|
|
785
|
+
workspaceId: wsId,
|
|
786
|
+
meta,
|
|
787
|
+
port,
|
|
788
|
+
message: alreadyCloned ? "Repository updated (git pull)" : "Repository cloned successfully"
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
} catch (err) {
|
|
792
|
+
console.error(`β Clone failed: ${err.message}`);
|
|
793
|
+
const hint = err.message.includes('Authentication') || err.message.includes('could not read')
|
|
794
|
+
? "Authentication failed. Ensure the repo is public or GitHub integration is configured."
|
|
795
|
+
: err.message.includes('not found') || err.message.includes('does not exist')
|
|
796
|
+
? "Repository not found. Check the URL is correct."
|
|
797
|
+
: "Clone failed. Ensure git is installed and the URL is accessible.";
|
|
798
|
+
res.status(500).json({ ok: false, error: err.message, hint });
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
|
|
737
802
|
/** Info do workspace */
|
|
738
803
|
app.get("/workspace/info", async (_req, res) => {
|
|
739
804
|
if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
|
|
@@ -4220,7 +4285,9 @@ app.post("/workspace/git/create-fix-branch", async (req, res) => {
|
|
|
4220
4285
|
.replace(/^https?:\/\/(.*@)?bitbucket\.org\//, '')
|
|
4221
4286
|
.replace(/^git@bitbucket\.org:/, '')
|
|
4222
4287
|
.replace(/\.git$/, '');
|
|
4223
|
-
|
|
4288
|
+
// β
FIXED: Bitbucket App Password auth β token jΓ‘ estΓ‘ no formato "username:appPassword"
|
|
4289
|
+
// x-token-auth nΓ£o funciona; Basic Auth via URL Γ© o mΓ©todo correcto
|
|
4290
|
+
authenticatedUrl = `https://${gitToken}@bitbucket.org/${repoPath}.git`;
|
|
4224
4291
|
} else if (remoteUrlRaw) {
|
|
4225
4292
|
// Unknown provider β try generic https with token
|
|
4226
4293
|
const urlObj = new URL(remoteUrlRaw.replace(/^git@([^:]+):/, 'https://$1/'));
|
|
@@ -4348,52 +4415,140 @@ app.post("/workspace/git/create-fix-branch", async (req, res) => {
|
|
|
4348
4415
|
} catch {}
|
|
4349
4416
|
|
|
4350
4417
|
// 11. Auto-create Pull Request if pushed and token available
|
|
4418
|
+
// β
FIXED: suporta GitHub, GitLab e Bitbucket; branding Insptech AI
|
|
4351
4419
|
let prUrl = '';
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
};
|
|
4420
|
+
const prBodyText =
|
|
4421
|
+
`## π€ Auto-fix by Insptech AI\n\n` +
|
|
4422
|
+
`**Branch:** \`${finalBranchName}\`\n` +
|
|
4423
|
+
`**Files changed:** ${status.split('\n').length}\n` +
|
|
4424
|
+
`**Commit:** ${commitSha.substring(0, 8)}\n\n` +
|
|
4425
|
+
`This pull request was automatically created by Insptech AI after detecting and fixing a production error.\n\n` +
|
|
4426
|
+
`### Changes\n` +
|
|
4427
|
+
status.split('\n').map(l => `- \`${l.trim()}\``).join('\n') + '\n\n' +
|
|
4428
|
+
`---\n*Generated by [Insptech AI](https://app.insptech.pt) β Autonomous Debugging Platform*`;
|
|
4429
|
+
|
|
4430
|
+
if (pushed && gitToken && remoteUrl) {
|
|
4431
|
+
|
|
4432
|
+
// ββ GitHub ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
4433
|
+
if (remoteUrl.includes('github.com')) {
|
|
4434
|
+
try {
|
|
4435
|
+
const repoPath = remoteUrl
|
|
4436
|
+
.replace(/^https?:\/\/(.*@)?github\.com\//, '')
|
|
4437
|
+
.replace(/^git@github\.com:/, '')
|
|
4438
|
+
.replace(/\.git$/, '');
|
|
4372
4439
|
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4440
|
+
const response = await fetch(`https://api.github.com/repos/${repoPath}/pulls`, {
|
|
4441
|
+
method: 'POST',
|
|
4442
|
+
headers: {
|
|
4443
|
+
'Authorization': `Bearer ${gitToken}`,
|
|
4444
|
+
'Accept': 'application/vnd.github+json',
|
|
4445
|
+
'Content-Type': 'application/json',
|
|
4446
|
+
'X-GitHub-Api-Version': '2022-11-28'
|
|
4447
|
+
},
|
|
4448
|
+
body: JSON.stringify({
|
|
4449
|
+
title: commitMessage,
|
|
4450
|
+
head: finalBranchName,
|
|
4451
|
+
base: targetBranch,
|
|
4452
|
+
body: prBodyText
|
|
4453
|
+
})
|
|
4454
|
+
});
|
|
4383
4455
|
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4456
|
+
if (response.ok) {
|
|
4457
|
+
const prData = await response.json();
|
|
4458
|
+
prUrl = prData.html_url;
|
|
4459
|
+
console.log(`β
GitHub PR created: ${prUrl}`);
|
|
4460
|
+
} else {
|
|
4461
|
+
const errText = await response.text();
|
|
4462
|
+
console.warn(`β οΈ GitHub PR creation failed (${response.status}): ${errText.substring(0, 200)}`);
|
|
4463
|
+
prUrl = mrUrl;
|
|
4464
|
+
}
|
|
4465
|
+
} catch (prErr) {
|
|
4466
|
+
console.warn(`β οΈ GitHub PR error: ${prErr.message}`);
|
|
4467
|
+
prUrl = mrUrl;
|
|
4468
|
+
}
|
|
4469
|
+
|
|
4470
|
+
// ββ GitLab ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
4471
|
+
} else if (remoteUrl.includes('gitlab.com') || remoteUrl.includes('gitlab')) {
|
|
4472
|
+
try {
|
|
4473
|
+
const hostMatch = remoteUrl.match(/https?:\/\/([^\/]+)/);
|
|
4474
|
+
const host = hostMatch ? hostMatch[1] : 'gitlab.com';
|
|
4475
|
+
const repoPath = remoteUrl
|
|
4476
|
+
.replace(/^https?:\/\/(.*@)?[^\/]+\//, '')
|
|
4477
|
+
.replace(/^git@[^:]+:/, '')
|
|
4478
|
+
.replace(/\.git$/, '');
|
|
4479
|
+
// GitLab API requer o path encoded (ex: "mygroup/myrepo" β "mygroup%2Fmyrepo")
|
|
4480
|
+
const encodedPath = encodeURIComponent(repoPath);
|
|
4481
|
+
|
|
4482
|
+
const response = await fetch(`https://${host}/api/v4/projects/${encodedPath}/merge_requests`, {
|
|
4483
|
+
method: 'POST',
|
|
4484
|
+
headers: {
|
|
4485
|
+
'PRIVATE-TOKEN': gitToken,
|
|
4486
|
+
'Content-Type': 'application/json'
|
|
4487
|
+
},
|
|
4488
|
+
body: JSON.stringify({
|
|
4489
|
+
title: commitMessage,
|
|
4490
|
+
description: prBodyText,
|
|
4491
|
+
source_branch: finalBranchName,
|
|
4492
|
+
target_branch: targetBranch,
|
|
4493
|
+
remove_source_branch: false
|
|
4494
|
+
})
|
|
4495
|
+
});
|
|
4496
|
+
|
|
4497
|
+
if (response.ok) {
|
|
4498
|
+
const mrData = await response.json();
|
|
4499
|
+
prUrl = mrData.web_url;
|
|
4500
|
+
console.log(`β
GitLab MR created: ${prUrl}`);
|
|
4501
|
+
} else {
|
|
4502
|
+
const errText = await response.text();
|
|
4503
|
+
console.warn(`β οΈ GitLab MR creation failed (${response.status}): ${errText.substring(0, 200)}`);
|
|
4504
|
+
prUrl = mrUrl;
|
|
4505
|
+
}
|
|
4506
|
+
} catch (glErr) {
|
|
4507
|
+
console.warn(`β οΈ GitLab MR error: ${glErr.message}`);
|
|
4508
|
+
prUrl = mrUrl;
|
|
4509
|
+
}
|
|
4510
|
+
|
|
4511
|
+
// ββ Bitbucket ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
4512
|
+
} else if (remoteUrl.includes('bitbucket.org') || remoteUrl.includes('bitbucket')) {
|
|
4513
|
+
try {
|
|
4514
|
+
const repoPath = remoteUrl
|
|
4515
|
+
.replace(/^https?:\/\/(.*@)?bitbucket\.org\//, '')
|
|
4516
|
+
.replace(/^git@bitbucket\.org:/, '')
|
|
4517
|
+
.replace(/\.git$/, '');
|
|
4518
|
+
const [workspace, repoSlug] = repoPath.split('/');
|
|
4519
|
+
|
|
4520
|
+
// gitToken Γ© "username:appPassword" β codificar para Basic Auth
|
|
4521
|
+
const credentials = Buffer.from(gitToken).toString('base64');
|
|
4522
|
+
|
|
4523
|
+
const response = await fetch(
|
|
4524
|
+
`https://api.bitbucket.org/2.0/repositories/${workspace}/${repoSlug}/pullrequests`, {
|
|
4525
|
+
method: 'POST',
|
|
4526
|
+
headers: {
|
|
4527
|
+
'Authorization': `Basic ${credentials}`,
|
|
4528
|
+
'Content-Type': 'application/json'
|
|
4529
|
+
},
|
|
4530
|
+
body: JSON.stringify({
|
|
4531
|
+
title: commitMessage,
|
|
4532
|
+
description: prBodyText,
|
|
4533
|
+
source: { branch: { name: finalBranchName } },
|
|
4534
|
+
destination: { branch: { name: targetBranch } },
|
|
4535
|
+
close_source_branch: false
|
|
4536
|
+
})
|
|
4537
|
+
});
|
|
4538
|
+
|
|
4539
|
+
if (response.ok) {
|
|
4540
|
+
const prData = await response.json();
|
|
4541
|
+
prUrl = prData.links && prData.links.html ? prData.links.html.href : mrUrl;
|
|
4542
|
+
console.log(`β
Bitbucket PR created: ${prUrl}`);
|
|
4543
|
+
} else {
|
|
4544
|
+
const errText = await response.text();
|
|
4545
|
+
console.warn(`β οΈ Bitbucket PR creation failed (${response.status}): ${errText.substring(0, 200)}`);
|
|
4546
|
+
prUrl = mrUrl;
|
|
4547
|
+
}
|
|
4548
|
+
} catch (bbErr) {
|
|
4549
|
+
console.warn(`β οΈ Bitbucket PR error: ${bbErr.message}`);
|
|
4392
4550
|
prUrl = mrUrl;
|
|
4393
4551
|
}
|
|
4394
|
-
} catch (prErr) {
|
|
4395
|
-
console.warn(`β οΈ PR creation error: ${prErr.message}`);
|
|
4396
|
-
prUrl = mrUrl;
|
|
4397
4552
|
}
|
|
4398
4553
|
}
|
|
4399
4554
|
|
|
@@ -4456,6 +4611,396 @@ app.get("/workspace/git/status", async (req, res) => {
|
|
|
4456
4611
|
}
|
|
4457
4612
|
});
|
|
4458
4613
|
|
|
4614
|
+
// ============================================
|
|
4615
|
+
// π¬ PULL REQUEST COMMENTS ENDPOINTS
|
|
4616
|
+
// Read, reply, and resolve PR comments via git providers
|
|
4617
|
+
// Used by the frontend "Code Review" tab in incident detail
|
|
4618
|
+
// ============================================
|
|
4619
|
+
|
|
4620
|
+
/**
|
|
4621
|
+
* GET /workspace/git/pr/comments
|
|
4622
|
+
*
|
|
4623
|
+
* List all comments for a pull request.
|
|
4624
|
+
* Detects provider (GitHub/Bitbucket) from remote URL.
|
|
4625
|
+
*
|
|
4626
|
+
* Query params:
|
|
4627
|
+
* - prNumber (required): PR/MR number
|
|
4628
|
+
* - filter: 'all' | 'unresolved' | 'review' (default: 'all')
|
|
4629
|
+
*/
|
|
4630
|
+
app.get("/workspace/git/pr/comments", async (req, res) => {
|
|
4631
|
+
if (!WORKSPACE_ROOT) {
|
|
4632
|
+
return res.status(400).json({ error: "workspace not set" });
|
|
4633
|
+
}
|
|
4634
|
+
|
|
4635
|
+
const { prNumber, filter = 'all' } = req.query;
|
|
4636
|
+
|
|
4637
|
+
if (!prNumber) {
|
|
4638
|
+
return res.status(400).json({ error: "prNumber query param is required" });
|
|
4639
|
+
}
|
|
4640
|
+
|
|
4641
|
+
try {
|
|
4642
|
+
const provider = await _getGitProvider();
|
|
4643
|
+
if (!provider) {
|
|
4644
|
+
return res.status(400).json({ error: "No git provider configured. Check remote URL and token." });
|
|
4645
|
+
}
|
|
4646
|
+
|
|
4647
|
+
const { owner, repo } = provider.info;
|
|
4648
|
+
let comments;
|
|
4649
|
+
|
|
4650
|
+
if (filter === 'unresolved') {
|
|
4651
|
+
comments = await provider.instance.listUnresolvedPRComments(owner, repo, Number(prNumber));
|
|
4652
|
+
} else if (filter === 'review') {
|
|
4653
|
+
comments = await provider.instance.listPRReviewComments(owner, repo, Number(prNumber));
|
|
4654
|
+
} else {
|
|
4655
|
+
// Get both general and review comments, merge and sort by date
|
|
4656
|
+
const [general, review] = await Promise.all([
|
|
4657
|
+
provider.instance.listPRComments(owner, repo, Number(prNumber)).catch(() => []),
|
|
4658
|
+
provider.instance.listPRReviewComments(owner, repo, Number(prNumber)).catch(() => [])
|
|
4659
|
+
]);
|
|
4660
|
+
|
|
4661
|
+
// Deduplicate by id
|
|
4662
|
+
const seen = new Set();
|
|
4663
|
+
comments = [...general, ...review].filter(c => {
|
|
4664
|
+
if (seen.has(c.id)) return false;
|
|
4665
|
+
seen.add(c.id);
|
|
4666
|
+
return true;
|
|
4667
|
+
});
|
|
4668
|
+
|
|
4669
|
+
// Sort by creation date
|
|
4670
|
+
comments.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
|
|
4671
|
+
}
|
|
4672
|
+
|
|
4673
|
+
res.json({
|
|
4674
|
+
ok: true,
|
|
4675
|
+
prNumber: Number(prNumber),
|
|
4676
|
+
filter,
|
|
4677
|
+
comments,
|
|
4678
|
+
total: comments.length,
|
|
4679
|
+
unresolvedCount: comments.filter(c => c.resolved === false || c.resolved === undefined).length
|
|
4680
|
+
});
|
|
4681
|
+
} catch (err) {
|
|
4682
|
+
console.error('β Failed to fetch PR comments:', err.message);
|
|
4683
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
4684
|
+
}
|
|
4685
|
+
});
|
|
4686
|
+
|
|
4687
|
+
/**
|
|
4688
|
+
* GET /workspace/git/pr/comments/:commentId
|
|
4689
|
+
*
|
|
4690
|
+
* Get a specific comment by ID.
|
|
4691
|
+
*
|
|
4692
|
+
* Query params:
|
|
4693
|
+
* - prNumber (required): PR/MR number
|
|
4694
|
+
*/
|
|
4695
|
+
app.get("/workspace/git/pr/comments/:commentId", async (req, res) => {
|
|
4696
|
+
if (!WORKSPACE_ROOT) {
|
|
4697
|
+
return res.status(400).json({ error: "workspace not set" });
|
|
4698
|
+
}
|
|
4699
|
+
|
|
4700
|
+
const { commentId } = req.params;
|
|
4701
|
+
const { prNumber } = req.query;
|
|
4702
|
+
|
|
4703
|
+
if (!prNumber) {
|
|
4704
|
+
return res.status(400).json({ error: "prNumber query param is required" });
|
|
4705
|
+
}
|
|
4706
|
+
|
|
4707
|
+
try {
|
|
4708
|
+
const provider = await _getGitProvider();
|
|
4709
|
+
if (!provider) {
|
|
4710
|
+
return res.status(400).json({ error: "No git provider configured" });
|
|
4711
|
+
}
|
|
4712
|
+
|
|
4713
|
+
const { owner, repo } = provider.info;
|
|
4714
|
+
const comment = await provider.instance.getPRComment(owner, repo, Number(prNumber), commentId);
|
|
4715
|
+
|
|
4716
|
+
res.json({ ok: true, comment });
|
|
4717
|
+
} catch (err) {
|
|
4718
|
+
console.error('β Failed to fetch PR comment:', err.message);
|
|
4719
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
4720
|
+
}
|
|
4721
|
+
});
|
|
4722
|
+
|
|
4723
|
+
/**
|
|
4724
|
+
* POST /workspace/git/pr/comments
|
|
4725
|
+
*
|
|
4726
|
+
* Add a comment to a pull request.
|
|
4727
|
+
*
|
|
4728
|
+
* Body:
|
|
4729
|
+
* - prNumber (required): PR/MR number
|
|
4730
|
+
* - body (required): Comment text (markdown)
|
|
4731
|
+
* - type: 'general' | 'review' (default: 'general')
|
|
4732
|
+
* - path: File path (required for type='review')
|
|
4733
|
+
* - line: Line number (required for type='review')
|
|
4734
|
+
* - side: 'LEFT' | 'RIGHT' (default: 'RIGHT')
|
|
4735
|
+
* - commitId: Commit SHA (for review comments)
|
|
4736
|
+
* - inReplyToId: Parent comment ID (for thread replies)
|
|
4737
|
+
*/
|
|
4738
|
+
app.post("/workspace/git/pr/comments", async (req, res) => {
|
|
4739
|
+
if (!WORKSPACE_ROOT) {
|
|
4740
|
+
return res.status(400).json({ error: "workspace not set" });
|
|
4741
|
+
}
|
|
4742
|
+
|
|
4743
|
+
const { prNumber, body, type = 'general', path: filePath, line, side, commitId, inReplyToId } = req.body;
|
|
4744
|
+
|
|
4745
|
+
if (!prNumber) {
|
|
4746
|
+
return res.status(400).json({ error: "prNumber is required" });
|
|
4747
|
+
}
|
|
4748
|
+
if (!body || !body.trim()) {
|
|
4749
|
+
return res.status(400).json({ error: "body is required" });
|
|
4750
|
+
}
|
|
4751
|
+
|
|
4752
|
+
try {
|
|
4753
|
+
const provider = await _getGitProvider();
|
|
4754
|
+
if (!provider) {
|
|
4755
|
+
return res.status(400).json({ error: "No git provider configured" });
|
|
4756
|
+
}
|
|
4757
|
+
|
|
4758
|
+
const { owner, repo } = provider.info;
|
|
4759
|
+
let comment;
|
|
4760
|
+
|
|
4761
|
+
if (inReplyToId) {
|
|
4762
|
+
// Reply to existing comment
|
|
4763
|
+
comment = await provider.instance.replyToPRComment(owner, repo, Number(prNumber), inReplyToId, body);
|
|
4764
|
+
} else if (type === 'review' && filePath) {
|
|
4765
|
+
// Inline code comment
|
|
4766
|
+
comment = await provider.instance.addPRReviewComment(owner, repo, Number(prNumber), {
|
|
4767
|
+
body,
|
|
4768
|
+
path: filePath,
|
|
4769
|
+
line: line ? Number(line) : null,
|
|
4770
|
+
side: side || 'RIGHT',
|
|
4771
|
+
commitId,
|
|
4772
|
+
inReplyToId
|
|
4773
|
+
});
|
|
4774
|
+
} else {
|
|
4775
|
+
// General PR comment
|
|
4776
|
+
comment = await provider.instance.addPRComment(owner, repo, Number(prNumber), body);
|
|
4777
|
+
}
|
|
4778
|
+
|
|
4779
|
+
console.log(`π¬ PR comment added to PR #${prNumber}: ${body.substring(0, 50)}...`);
|
|
4780
|
+
res.json({ ok: true, comment });
|
|
4781
|
+
} catch (err) {
|
|
4782
|
+
console.error('β Failed to add PR comment:', err.message);
|
|
4783
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
4784
|
+
}
|
|
4785
|
+
});
|
|
4786
|
+
|
|
4787
|
+
/**
|
|
4788
|
+
* POST /workspace/git/pr/comments/:commentId/resolve
|
|
4789
|
+
*
|
|
4790
|
+
* Mark a comment/thread as resolved.
|
|
4791
|
+
*
|
|
4792
|
+
* Body:
|
|
4793
|
+
* - prNumber (required): PR/MR number
|
|
4794
|
+
*/
|
|
4795
|
+
app.post("/workspace/git/pr/comments/:commentId/resolve", async (req, res) => {
|
|
4796
|
+
if (!WORKSPACE_ROOT) {
|
|
4797
|
+
return res.status(400).json({ error: "workspace not set" });
|
|
4798
|
+
}
|
|
4799
|
+
|
|
4800
|
+
const { commentId } = req.params;
|
|
4801
|
+
const { prNumber } = req.body;
|
|
4802
|
+
|
|
4803
|
+
if (!prNumber) {
|
|
4804
|
+
return res.status(400).json({ error: "prNumber is required" });
|
|
4805
|
+
}
|
|
4806
|
+
|
|
4807
|
+
try {
|
|
4808
|
+
const provider = await _getGitProvider();
|
|
4809
|
+
if (!provider) {
|
|
4810
|
+
return res.status(400).json({ error: "No git provider configured" });
|
|
4811
|
+
}
|
|
4812
|
+
|
|
4813
|
+
const { owner, repo } = provider.info;
|
|
4814
|
+
const result = await provider.instance.resolvePRComment(owner, repo, Number(prNumber), commentId);
|
|
4815
|
+
|
|
4816
|
+
console.log(`β
PR comment ${commentId} resolved on PR #${prNumber}`);
|
|
4817
|
+
res.json({ ok: true, commentId, ...result });
|
|
4818
|
+
} catch (err) {
|
|
4819
|
+
console.error('β Failed to resolve PR comment:', err.message);
|
|
4820
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
4821
|
+
}
|
|
4822
|
+
});
|
|
4823
|
+
|
|
4824
|
+
/**
|
|
4825
|
+
* POST /workspace/git/pr/comments/:commentId/unresolve
|
|
4826
|
+
*
|
|
4827
|
+
* Mark a comment/thread as unresolved.
|
|
4828
|
+
*
|
|
4829
|
+
* Body:
|
|
4830
|
+
* - prNumber (required): PR/MR number
|
|
4831
|
+
*/
|
|
4832
|
+
app.post("/workspace/git/pr/comments/:commentId/unresolve", async (req, res) => {
|
|
4833
|
+
if (!WORKSPACE_ROOT) {
|
|
4834
|
+
return res.status(400).json({ error: "workspace not set" });
|
|
4835
|
+
}
|
|
4836
|
+
|
|
4837
|
+
const { commentId } = req.params;
|
|
4838
|
+
const { prNumber } = req.body;
|
|
4839
|
+
|
|
4840
|
+
if (!prNumber) {
|
|
4841
|
+
return res.status(400).json({ error: "prNumber is required" });
|
|
4842
|
+
}
|
|
4843
|
+
|
|
4844
|
+
try {
|
|
4845
|
+
const provider = await _getGitProvider();
|
|
4846
|
+
if (!provider) {
|
|
4847
|
+
return res.status(400).json({ error: "No git provider configured" });
|
|
4848
|
+
}
|
|
4849
|
+
|
|
4850
|
+
const { owner, repo } = provider.info;
|
|
4851
|
+
const result = await provider.instance.unresolvePRComment(owner, repo, Number(prNumber), commentId);
|
|
4852
|
+
|
|
4853
|
+
console.log(`π PR comment ${commentId} unresolve on PR #${prNumber}`);
|
|
4854
|
+
res.json({ ok: true, commentId, ...result });
|
|
4855
|
+
} catch (err) {
|
|
4856
|
+
console.error('β Failed to unresolve PR comment:', err.message);
|
|
4857
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
4858
|
+
}
|
|
4859
|
+
});
|
|
4860
|
+
|
|
4861
|
+
/**
|
|
4862
|
+
* POST /workspace/git/pr/comments/:commentId/fix-and-resolve
|
|
4863
|
+
*
|
|
4864
|
+
* AI-powered: Read the comment, understand the request, apply the code fix,
|
|
4865
|
+
* commit, reply with explanation, and resolve the thread.
|
|
4866
|
+
*
|
|
4867
|
+
* This is the "magic" endpoint that makes DeepDebug unique.
|
|
4868
|
+
*
|
|
4869
|
+
* Body:
|
|
4870
|
+
* - prNumber (required): PR/MR number
|
|
4871
|
+
* - branch: Branch to apply fix on (defaults to current branch)
|
|
4872
|
+
*/
|
|
4873
|
+
app.post("/workspace/git/pr/comments/:commentId/fix-and-resolve", async (req, res) => {
|
|
4874
|
+
if (!WORKSPACE_ROOT) {
|
|
4875
|
+
return res.status(400).json({ error: "workspace not set" });
|
|
4876
|
+
}
|
|
4877
|
+
|
|
4878
|
+
const { commentId } = req.params;
|
|
4879
|
+
const { prNumber, branch } = req.body;
|
|
4880
|
+
|
|
4881
|
+
if (!prNumber) {
|
|
4882
|
+
return res.status(400).json({ error: "prNumber is required" });
|
|
4883
|
+
}
|
|
4884
|
+
|
|
4885
|
+
try {
|
|
4886
|
+
const provider = await _getGitProvider();
|
|
4887
|
+
if (!provider) {
|
|
4888
|
+
return res.status(400).json({ error: "No git provider configured" });
|
|
4889
|
+
}
|
|
4890
|
+
|
|
4891
|
+
const { owner, repo } = provider.info;
|
|
4892
|
+
|
|
4893
|
+
// 1. Fetch the comment
|
|
4894
|
+
const comment = await provider.instance.getPRComment(owner, repo, Number(prNumber), commentId);
|
|
4895
|
+
|
|
4896
|
+
if (!comment) {
|
|
4897
|
+
return res.status(404).json({ error: "Comment not found" });
|
|
4898
|
+
}
|
|
4899
|
+
|
|
4900
|
+
console.log(`π€ AI Fix & Resolve: PR #${prNumber}, comment ${commentId}`);
|
|
4901
|
+
console.log(` File: ${comment.path || '(general)'}, Line: ${comment.line || 'N/A'}`);
|
|
4902
|
+
console.log(` Request: ${comment.body.substring(0, 100)}...`);
|
|
4903
|
+
|
|
4904
|
+
// 2. Return the comment info for the Gateway to orchestrate the AI fix
|
|
4905
|
+
// The actual AI analysis + patch is handled by the Gateway's AI service,
|
|
4906
|
+
// not the Local Agent. We provide the context here.
|
|
4907
|
+
res.json({
|
|
4908
|
+
ok: true,
|
|
4909
|
+
commentId,
|
|
4910
|
+
prNumber: Number(prNumber),
|
|
4911
|
+
comment: {
|
|
4912
|
+
id: comment.id,
|
|
4913
|
+
body: comment.body,
|
|
4914
|
+
path: comment.path,
|
|
4915
|
+
line: comment.line,
|
|
4916
|
+
side: comment.side,
|
|
4917
|
+
author: comment.authorUsername,
|
|
4918
|
+
type: comment.type,
|
|
4919
|
+
resolved: comment.resolved
|
|
4920
|
+
},
|
|
4921
|
+
context: {
|
|
4922
|
+
owner,
|
|
4923
|
+
repo,
|
|
4924
|
+
branch: branch || null,
|
|
4925
|
+
provider: provider.instance.getId()
|
|
4926
|
+
},
|
|
4927
|
+
// Action hint for the Gateway
|
|
4928
|
+
action: 'fix_and_resolve',
|
|
4929
|
+
message: 'Comment fetched. Gateway should now: 1) Read file, 2) AI analyze, 3) Apply patch, 4) Commit, 5) Reply, 6) Resolve'
|
|
4930
|
+
});
|
|
4931
|
+
} catch (err) {
|
|
4932
|
+
console.error('β Failed to prepare fix-and-resolve:', err.message);
|
|
4933
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
4934
|
+
}
|
|
4935
|
+
});
|
|
4936
|
+
|
|
4937
|
+
|
|
4938
|
+
// ============================================
|
|
4939
|
+
// π§ GIT PROVIDER HELPER (internal)
|
|
4940
|
+
// Detects provider from remote URL and configures with token
|
|
4941
|
+
// ============================================
|
|
4942
|
+
|
|
4943
|
+
async function _getGitProvider() {
|
|
4944
|
+
if (!WORKSPACE_ROOT) return null;
|
|
4945
|
+
|
|
4946
|
+
try {
|
|
4947
|
+
const { execSync } = await import('child_process');
|
|
4948
|
+
const opts = { cwd: WORKSPACE_ROOT, encoding: 'utf8', timeout: 5000 };
|
|
4949
|
+
|
|
4950
|
+
// Get remote URL
|
|
4951
|
+
let remoteUrl;
|
|
4952
|
+
try {
|
|
4953
|
+
remoteUrl = execSync('git remote get-url origin', opts).trim();
|
|
4954
|
+
} catch {
|
|
4955
|
+
console.warn('β οΈ No git remote found');
|
|
4956
|
+
return null;
|
|
4957
|
+
}
|
|
4958
|
+
|
|
4959
|
+
// Detect provider from URL
|
|
4960
|
+
const { getGitProviderRegistry } = await import('./git/git-provider-registry.js');
|
|
4961
|
+
const registry = getGitProviderRegistry();
|
|
4962
|
+
const providerId = registry.detectProviderFromUrl(remoteUrl);
|
|
4963
|
+
|
|
4964
|
+
if (!providerId) {
|
|
4965
|
+
console.warn(`β οΈ Unknown git provider for URL: ${remoteUrl}`);
|
|
4966
|
+
return null;
|
|
4967
|
+
}
|
|
4968
|
+
|
|
4969
|
+
// Get token from environment
|
|
4970
|
+
let token = null;
|
|
4971
|
+
if (providerId === 'github') {
|
|
4972
|
+
token = process.env.GITHUB_TOKEN || process.env.GIT_TOKEN;
|
|
4973
|
+
} else if (providerId === 'bitbucket') {
|
|
4974
|
+
token = process.env.BITBUCKET_TOKEN || process.env.BITBUCKET_APP_PASSWORD || process.env.GIT_TOKEN;
|
|
4975
|
+
} else if (providerId === 'gitlab') {
|
|
4976
|
+
token = process.env.GITLAB_TOKEN || process.env.GIT_TOKEN;
|
|
4977
|
+
}
|
|
4978
|
+
|
|
4979
|
+
if (!token) {
|
|
4980
|
+
console.warn(`β οΈ No token found for ${providerId}. Set ${providerId.toUpperCase()}_TOKEN or GIT_TOKEN env var.`);
|
|
4981
|
+
return null;
|
|
4982
|
+
}
|
|
4983
|
+
|
|
4984
|
+
// Create provider instance
|
|
4985
|
+
const config = { accessToken: token };
|
|
4986
|
+
|
|
4987
|
+
// For Bitbucket App Password auth, add username
|
|
4988
|
+
if (providerId === 'bitbucket' && process.env.BITBUCKET_USERNAME) {
|
|
4989
|
+
config.username = process.env.BITBUCKET_USERNAME;
|
|
4990
|
+
config.authMode = 'app_password';
|
|
4991
|
+
}
|
|
4992
|
+
|
|
4993
|
+
const instance = registry.create(providerId, config);
|
|
4994
|
+
|
|
4995
|
+
// Parse owner/repo from URL
|
|
4996
|
+
const info = instance.parseRepositoryUrl(remoteUrl);
|
|
4997
|
+
|
|
4998
|
+
return { instance, info, providerId, remoteUrl };
|
|
4999
|
+
} catch (err) {
|
|
5000
|
+
console.error('β Failed to initialize git provider:', err.message);
|
|
5001
|
+
return null;
|
|
5002
|
+
}
|
|
5003
|
+
}
|
|
4459
5004
|
// ============================================
|
|
4460
5005
|
// π MULTI-WORKSPACE ENDPOINTS
|
|
4461
5006
|
// ============================================
|