claude-teammate 0.1.83 → 0.1.85
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/package.json +1 -1
- package/src/commands/start.js +32 -16
- package/src/commands/worker.js +45 -28
- package/src/config.js +21 -1
- package/src/forge/gitlab.js +102 -45
- package/src/forge/index.js +54 -25
package/package.json
CHANGED
package/src/commands/start.js
CHANGED
|
@@ -6,6 +6,7 @@ import process from "node:process";
|
|
|
6
6
|
import { ensurePlaywrightMcpAvailable, verifyClaudeCli } from "../claude.js";
|
|
7
7
|
import { createForgeRegistry } from "../forge/index.js";
|
|
8
8
|
import { createJiraClient } from "../jira.js";
|
|
9
|
+
import { listKnownRepos } from "../memory.js";
|
|
9
10
|
import {
|
|
10
11
|
REQUIRED_FIELDS,
|
|
11
12
|
ensureGlobalConfigDir,
|
|
@@ -71,7 +72,7 @@ export async function runStartCommand({ projectRoot, entrypointPath, args = [] }
|
|
|
71
72
|
|
|
72
73
|
process.stdout.write("Checking Jira credentials...\n");
|
|
73
74
|
await verifyJiraCredentials(values);
|
|
74
|
-
await verifyForgeCredentials(values);
|
|
75
|
+
await verifyForgeCredentials(values, configRoot);
|
|
75
76
|
|
|
76
77
|
const logFd = openSync(runtimePaths.logFile, "a");
|
|
77
78
|
const child = spawn(process.execPath, [entrypointPath, "run-worker", configRoot], {
|
|
@@ -176,31 +177,46 @@ async function verifyJiraCredentials(config) {
|
|
|
176
177
|
}
|
|
177
178
|
}
|
|
178
179
|
|
|
179
|
-
async function verifyForgeCredentials(config) {
|
|
180
|
+
async function verifyForgeCredentials(config, configRoot) {
|
|
180
181
|
const forge = createForgeRegistry(config);
|
|
181
|
-
const
|
|
182
|
+
const provider = forge.getActiveClient();
|
|
182
183
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
184
|
+
if (!provider) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const verificationTargets = await getForgeVerificationTargets(provider.id, config, configRoot);
|
|
189
|
+
if (verificationTargets.length === 0) {
|
|
190
|
+
process.stdout.write(`Skipping ${provider.label} credential preflight.\n`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
188
193
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
throw new Error(`${provider.label} credential check failed: ${formatError(error)}`);
|
|
194
|
+
process.stdout.write(`Checking ${provider.label} credentials...\n`);
|
|
195
|
+
try {
|
|
196
|
+
for (const target of verificationTargets) {
|
|
197
|
+
await provider.client.fetchCurrentUser(target);
|
|
194
198
|
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
throw new Error(`${provider.label} credential check failed: ${formatError(error)}`);
|
|
195
201
|
}
|
|
196
202
|
}
|
|
197
203
|
|
|
198
|
-
export function
|
|
204
|
+
export async function getForgeVerificationTargets(providerId, config, configRoot) {
|
|
199
205
|
if (providerId === "gitlab") {
|
|
200
|
-
|
|
206
|
+
const configuredBaseUrl = String(config.GITLAB_BASE_URL || "").trim();
|
|
207
|
+
if (configuredBaseUrl) {
|
|
208
|
+
return [configuredBaseUrl];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const repos = await listKnownRepos(configRoot);
|
|
212
|
+
return [...new Set(
|
|
213
|
+
repos
|
|
214
|
+
.map((repo) => String(repo?.url || "").trim())
|
|
215
|
+
.filter((repoUrl) => repoUrl.includes("gitlab"))
|
|
216
|
+
)];
|
|
201
217
|
}
|
|
202
218
|
|
|
203
|
-
return
|
|
219
|
+
return [""];
|
|
204
220
|
}
|
|
205
221
|
|
|
206
222
|
function formatError(error) {
|
package/src/commands/worker.js
CHANGED
|
@@ -125,7 +125,7 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
125
125
|
const jira = createJiraClient(values);
|
|
126
126
|
const forgeRegistry = createForgeRegistry(values);
|
|
127
127
|
const jiraBotUser = await loadJiraBotUser(jira, values, logger);
|
|
128
|
-
const forgeBotUsers =
|
|
128
|
+
const forgeBotUsers = new Map();
|
|
129
129
|
const state = {
|
|
130
130
|
...previousState,
|
|
131
131
|
projectRoot,
|
|
@@ -239,12 +239,12 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
239
239
|
}
|
|
240
240
|
githubFlag.value = true;
|
|
241
241
|
await runPoll("GitHub", "GitHub", state, async () => {
|
|
242
|
-
const repos = await listKnownRepos(projectRoot);
|
|
242
|
+
const repos = filterReposForActiveForge(await listKnownRepos(projectRoot), forgeRegistry);
|
|
243
243
|
const processedIssues = [];
|
|
244
244
|
let trackedIssueCount = 0;
|
|
245
245
|
for (const repo of repos) {
|
|
246
246
|
const provider = forgeRegistry.getClientForRepo(repo.url);
|
|
247
|
-
const botUser = getForgeBotUserForRepo(forgeBotUsers, repo.url);
|
|
247
|
+
const botUser = await getForgeBotUserForRepo(forgeRegistry, forgeBotUsers, repo.url, logger);
|
|
248
248
|
const localPath = await resolveRepoLocalPath(repo, runtimePaths.reposDir, logger, `${provider.label} repo`);
|
|
249
249
|
if (localPath === null) {
|
|
250
250
|
continue;
|
|
@@ -283,12 +283,12 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
283
283
|
prFlag.value = true;
|
|
284
284
|
const prJira = createJiraClient(values);
|
|
285
285
|
await runPoll("Pr", "Pull request", state, async () => {
|
|
286
|
-
const repos = await listKnownRepos(projectRoot);
|
|
286
|
+
const repos = filterReposForActiveForge(await listKnownRepos(projectRoot), forgeRegistry);
|
|
287
287
|
const processedPrs = [];
|
|
288
288
|
let trackedPrCount = 0;
|
|
289
289
|
for (const repo of repos) {
|
|
290
290
|
const provider = forgeRegistry.getClientForRepo(repo.url);
|
|
291
|
-
const botUser = getForgeBotUserForRepo(forgeBotUsers, repo.url);
|
|
291
|
+
const botUser = await getForgeBotUserForRepo(forgeRegistry, forgeBotUsers, repo.url, logger);
|
|
292
292
|
const localPath = await resolveRepoLocalPath(repo, runtimePaths.reposDir, logger, "draft PR repo");
|
|
293
293
|
if (localPath === null) {
|
|
294
294
|
continue;
|
|
@@ -330,7 +330,10 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
330
330
|
reviewFlag.value = true;
|
|
331
331
|
await runPoll("Review", "PR review", state, async () => {
|
|
332
332
|
const processedPrs = [];
|
|
333
|
-
const
|
|
333
|
+
const repos = filterReposForActiveForge(await listKnownRepos(projectRoot), forgeRegistry);
|
|
334
|
+
const prs = await forgeRegistry.listPrsNeedingReview({
|
|
335
|
+
repoUrls: repos.map((repo) => repo.url)
|
|
336
|
+
});
|
|
334
337
|
let reviewedPrCount = prs.length;
|
|
335
338
|
for (const pr of prs) {
|
|
336
339
|
if (!pr.repoUrl) {
|
|
@@ -451,6 +454,7 @@ async function processJiraIssue({ issue, jira, forgeRegistry, botUser, config, p
|
|
|
451
454
|
const epicMemoryRecord = await loadEpicMemory(projectRoot, config.JIRA_BASE_URL, detail);
|
|
452
455
|
let epicMemory = epicMemoryRecord.data;
|
|
453
456
|
let issueMemory = issueMemoryRecord.data;
|
|
457
|
+
epicMemory.repos = filterReposForActiveForge(epicMemory.repos, forgeRegistry);
|
|
454
458
|
|
|
455
459
|
try {
|
|
456
460
|
issueMemory.last_error = "";
|
|
@@ -477,6 +481,7 @@ async function processJiraIssue({ issue, jira, forgeRegistry, botUser, config, p
|
|
|
477
481
|
|
|
478
482
|
if (repoUrlsFromComments.length > 0) {
|
|
479
483
|
epicMemory.repos = mergeRepoUrls(epicMemory.repos, repoUrlsFromComments);
|
|
484
|
+
epicMemory.repos = filterReposForActiveForge(epicMemory.repos, forgeRegistry);
|
|
480
485
|
if (
|
|
481
486
|
issueMemory.workflow_state === "awaiting_repo_request" ||
|
|
482
487
|
issueMemory.workflow_state === "awaiting_repo_response" ||
|
|
@@ -1630,30 +1635,27 @@ async function loadJiraBotUser(jira, config, logger) {
|
|
|
1630
1635
|
}
|
|
1631
1636
|
}
|
|
1632
1637
|
|
|
1633
|
-
async function
|
|
1634
|
-
const
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
users.set(provider.id, await provider.client.fetchCurrentUser());
|
|
1639
|
-
} catch (error) {
|
|
1640
|
-
await logger.error(`${provider.label} bot identity lookup failed`, { error });
|
|
1641
|
-
users.set(provider.id, {
|
|
1642
|
-
login: null,
|
|
1643
|
-
id: null
|
|
1644
|
-
});
|
|
1645
|
-
}
|
|
1638
|
+
async function getForgeBotUserForRepo(forgeRegistry, forgeBotUsers, repoUrl, logger) {
|
|
1639
|
+
const repo = parseRepoUrl(repoUrl);
|
|
1640
|
+
const cacheKey = `${repo.provider}:${repo.host}`;
|
|
1641
|
+
if (forgeBotUsers.has(cacheKey)) {
|
|
1642
|
+
return forgeBotUsers.get(cacheKey);
|
|
1646
1643
|
}
|
|
1647
1644
|
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1645
|
+
const provider = forgeRegistry.getClientForRepo(repoUrl);
|
|
1646
|
+
try {
|
|
1647
|
+
const user = await provider.fetchCurrentUser(repoUrl);
|
|
1648
|
+
forgeBotUsers.set(cacheKey, user);
|
|
1649
|
+
return user;
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
await logger.error(`${provider.label} bot identity lookup failed`, { repo: repoUrl, error });
|
|
1652
|
+
const fallback = {
|
|
1653
|
+
login: null,
|
|
1654
|
+
id: null
|
|
1655
|
+
};
|
|
1656
|
+
forgeBotUsers.set(cacheKey, fallback);
|
|
1657
|
+
return fallback;
|
|
1658
|
+
}
|
|
1657
1659
|
}
|
|
1658
1660
|
|
|
1659
1661
|
function isToDoStatus(status) {
|
|
@@ -2234,6 +2236,21 @@ function mergeRepoUrls(existingRepos, repoUrls) {
|
|
|
2234
2236
|
return merged;
|
|
2235
2237
|
}
|
|
2236
2238
|
|
|
2239
|
+
function filterReposForActiveForge(repos, forgeRegistry) {
|
|
2240
|
+
const activeProviderId = forgeRegistry.getActiveProviderId();
|
|
2241
|
+
if (!activeProviderId) {
|
|
2242
|
+
return [];
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
return (Array.isArray(repos) ? repos : []).filter((repo) => {
|
|
2246
|
+
try {
|
|
2247
|
+
return parseRepoUrl(repo?.url || "").provider === activeProviderId;
|
|
2248
|
+
} catch {
|
|
2249
|
+
return false;
|
|
2250
|
+
}
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2237
2254
|
function dedupeGitHubIssues(githubIssues) {
|
|
2238
2255
|
const seen = new Set();
|
|
2239
2256
|
const deduped = [];
|
package/src/config.js
CHANGED
|
@@ -34,6 +34,12 @@ export const REQUIRED_FIELDS = [
|
|
|
34
34
|
secret: true,
|
|
35
35
|
required: true
|
|
36
36
|
},
|
|
37
|
+
{
|
|
38
|
+
key: "FORGE_PROVIDER",
|
|
39
|
+
prompt: "Forge provider (gitlab or github)",
|
|
40
|
+
example: "gitlab",
|
|
41
|
+
required: false
|
|
42
|
+
},
|
|
37
43
|
{
|
|
38
44
|
key: "GITHUB_PAT",
|
|
39
45
|
prompt: "GitHub personal access token",
|
|
@@ -94,7 +100,16 @@ export function getMissingFields(values) {
|
|
|
94
100
|
.filter((field) => field.required !== false && !values[field.key])
|
|
95
101
|
.map((field) => field.key);
|
|
96
102
|
|
|
97
|
-
|
|
103
|
+
const provider = normalizeForgeProvider(values.FORGE_PROVIDER);
|
|
104
|
+
if (provider === "github") {
|
|
105
|
+
if (!String(values.GITHUB_PAT || "").trim()) {
|
|
106
|
+
missing.push("GITHUB_PAT");
|
|
107
|
+
}
|
|
108
|
+
} else if (provider === "gitlab") {
|
|
109
|
+
if (!String(values.GITLAB_TOKEN || "").trim()) {
|
|
110
|
+
missing.push("GITLAB_TOKEN");
|
|
111
|
+
}
|
|
112
|
+
} else if (!hasConfiguredForgeToken(values)) {
|
|
98
113
|
missing.push("GITHUB_PAT or GITLAB_TOKEN");
|
|
99
114
|
}
|
|
100
115
|
|
|
@@ -105,6 +120,11 @@ export function hasConfiguredForgeToken(values) {
|
|
|
105
120
|
return Boolean(String(values.GITHUB_PAT || "").trim() || String(values.GITLAB_TOKEN || "").trim());
|
|
106
121
|
}
|
|
107
122
|
|
|
123
|
+
export function normalizeForgeProvider(value) {
|
|
124
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
125
|
+
return normalized === "gitlab" || normalized === "github" ? normalized : "";
|
|
126
|
+
}
|
|
127
|
+
|
|
108
128
|
export function maskSecret(value) {
|
|
109
129
|
if (value.length <= 4) {
|
|
110
130
|
return "*".repeat(value.length);
|
package/src/forge/gitlab.js
CHANGED
|
@@ -7,8 +7,10 @@ export function createGitLabClient(config) {
|
|
|
7
7
|
id: "gitlab",
|
|
8
8
|
label: "GitLab",
|
|
9
9
|
|
|
10
|
-
async fetchCurrentUser() {
|
|
11
|
-
const payload = await requestGitLab(config, "/api/v4/user"
|
|
10
|
+
async fetchCurrentUser(repoUrl = "") {
|
|
11
|
+
const payload = await requestGitLab(config, "/api/v4/user", {
|
|
12
|
+
baseUrl: resolveGitLabBaseUrl(config, repoUrl)
|
|
13
|
+
});
|
|
12
14
|
return {
|
|
13
15
|
login: payload.username ?? null,
|
|
14
16
|
id: payload.id ?? null
|
|
@@ -17,7 +19,7 @@ export function createGitLabClient(config) {
|
|
|
17
19
|
|
|
18
20
|
async findIssueByJiraKey(repoUrl, jiraKey) {
|
|
19
21
|
const repo = parseGitLabRepoUrl(repoUrl);
|
|
20
|
-
const payload = await
|
|
22
|
+
const payload = await requestGitLabProject(config, repo, "/issues", {
|
|
21
23
|
searchParams: {
|
|
22
24
|
state: "all",
|
|
23
25
|
search: jiraKey,
|
|
@@ -38,7 +40,7 @@ export function createGitLabClient(config) {
|
|
|
38
40
|
|
|
39
41
|
async createIssue(repoUrl, issue) {
|
|
40
42
|
const repo = parseGitLabRepoUrl(repoUrl);
|
|
41
|
-
const payload = await
|
|
43
|
+
const payload = await requestGitLabProject(config, repo, "/issues", {
|
|
42
44
|
method: "POST",
|
|
43
45
|
body: {
|
|
44
46
|
title: issue.title,
|
|
@@ -53,8 +55,8 @@ export function createGitLabClient(config) {
|
|
|
53
55
|
|
|
54
56
|
async fetchIssue(repoUrl, issueNumber) {
|
|
55
57
|
const repo = parseGitLabRepoUrl(repoUrl);
|
|
56
|
-
const payload = await
|
|
57
|
-
const comments = await
|
|
58
|
+
const payload = await requestGitLabProject(config, repo, `/issues/${issueNumber}`);
|
|
59
|
+
const comments = await requestGitLabProject(config, repo, `/issues/${issueNumber}/notes`, {
|
|
58
60
|
searchParams: {
|
|
59
61
|
sort: "asc",
|
|
60
62
|
per_page: "100"
|
|
@@ -66,7 +68,7 @@ export function createGitLabClient(config) {
|
|
|
66
68
|
|
|
67
69
|
async listIssues(repoUrl) {
|
|
68
70
|
const repo = parseGitLabRepoUrl(repoUrl);
|
|
69
|
-
const payload = await
|
|
71
|
+
const payload = await requestGitLabProject(config, repo, "/issues", {
|
|
70
72
|
searchParams: {
|
|
71
73
|
state: "opened",
|
|
72
74
|
per_page: "100"
|
|
@@ -87,7 +89,7 @@ export function createGitLabClient(config) {
|
|
|
87
89
|
|
|
88
90
|
async listPullRequests(repoUrl) {
|
|
89
91
|
const repo = parseGitLabRepoUrl(repoUrl);
|
|
90
|
-
const payload = await
|
|
92
|
+
const payload = await requestGitLabProject(config, repo, "/merge_requests", {
|
|
91
93
|
searchParams: {
|
|
92
94
|
state: "opened",
|
|
93
95
|
per_page: "100"
|
|
@@ -100,8 +102,8 @@ export function createGitLabClient(config) {
|
|
|
100
102
|
|
|
101
103
|
async fetchPullRequest(repoUrl, pullNumber) {
|
|
102
104
|
const repo = parseGitLabRepoUrl(repoUrl);
|
|
103
|
-
const payload = await
|
|
104
|
-
const comments = await
|
|
105
|
+
const payload = await requestGitLabProject(config, repo, `/merge_requests/${pullNumber}`);
|
|
106
|
+
const comments = await requestGitLabProject(config, repo, `/merge_requests/${pullNumber}/notes`, {
|
|
105
107
|
searchParams: {
|
|
106
108
|
sort: "asc",
|
|
107
109
|
per_page: "100"
|
|
@@ -110,7 +112,9 @@ export function createGitLabClient(config) {
|
|
|
110
112
|
let headRepoCloneUrl = null;
|
|
111
113
|
if (payload.source_project_id) {
|
|
112
114
|
try {
|
|
113
|
-
const sourceProject = await requestGitLab(config, `/api/v4/projects/${payload.source_project_id}
|
|
115
|
+
const sourceProject = await requestGitLab(config, `/api/v4/projects/${payload.source_project_id}`, {
|
|
116
|
+
baseUrl: `${repo.origin}/`
|
|
117
|
+
});
|
|
114
118
|
headRepoCloneUrl = sourceProject.http_url_to_repo || null;
|
|
115
119
|
} catch {
|
|
116
120
|
headRepoCloneUrl = null;
|
|
@@ -122,7 +126,7 @@ export function createGitLabClient(config) {
|
|
|
122
126
|
|
|
123
127
|
async updateIssue(repoUrl, issueNumber, updates) {
|
|
124
128
|
const repo = parseGitLabRepoUrl(repoUrl);
|
|
125
|
-
const payload = await
|
|
129
|
+
const payload = await requestGitLabProject(config, repo, `/issues/${issueNumber}`, {
|
|
126
130
|
method: "PUT",
|
|
127
131
|
body: {
|
|
128
132
|
title: updates.title,
|
|
@@ -134,7 +138,7 @@ export function createGitLabClient(config) {
|
|
|
134
138
|
|
|
135
139
|
async postIssueComment(repoUrl, issueNumber, body) {
|
|
136
140
|
const repo = parseGitLabRepoUrl(repoUrl);
|
|
137
|
-
const payload = await
|
|
141
|
+
const payload = await requestGitLabProject(config, repo, issueNotePath(issueNumber), {
|
|
138
142
|
method: "POST",
|
|
139
143
|
body: { body }
|
|
140
144
|
});
|
|
@@ -143,7 +147,7 @@ export function createGitLabClient(config) {
|
|
|
143
147
|
|
|
144
148
|
async createIssueReaction(repoUrl, issueNumber, content = "eyes") {
|
|
145
149
|
const repo = parseGitLabRepoUrl(repoUrl);
|
|
146
|
-
return
|
|
150
|
+
return requestGitLabProject(config, repo, `${issueNotePath(issueNumber)}/award_emoji`, {
|
|
147
151
|
method: "POST",
|
|
148
152
|
body: {
|
|
149
153
|
name: mapReaction(content)
|
|
@@ -163,6 +167,7 @@ export function createGitLabClient(config) {
|
|
|
163
167
|
for (const candidate of candidates) {
|
|
164
168
|
try {
|
|
165
169
|
return await requestGitLab(config, candidate, {
|
|
170
|
+
baseUrl: `${repo.origin}/`,
|
|
166
171
|
method: "POST",
|
|
167
172
|
body
|
|
168
173
|
});
|
|
@@ -177,6 +182,7 @@ export function createGitLabClient(config) {
|
|
|
177
182
|
async isRepoCollaborator(repoUrl, username) {
|
|
178
183
|
const repo = parseGitLabRepoUrl(repoUrl);
|
|
179
184
|
const users = await requestGitLab(config, "/api/v4/users", {
|
|
185
|
+
baseUrl: resolveGitLabBaseUrl(config, repoUrl),
|
|
180
186
|
searchParams: {
|
|
181
187
|
username,
|
|
182
188
|
per_page: "1"
|
|
@@ -191,7 +197,7 @@ export function createGitLabClient(config) {
|
|
|
191
197
|
}
|
|
192
198
|
|
|
193
199
|
try {
|
|
194
|
-
await
|
|
200
|
+
await requestGitLabProject(config, repo, `/members/all/${user.id}`);
|
|
195
201
|
return true;
|
|
196
202
|
} catch (error) {
|
|
197
203
|
if (String(error.message || "").includes("returned 404")) {
|
|
@@ -210,7 +216,7 @@ export function createGitLabClient(config) {
|
|
|
210
216
|
if (updates.state === "closed") {
|
|
211
217
|
body.state_event = "close";
|
|
212
218
|
}
|
|
213
|
-
const payload = await
|
|
219
|
+
const payload = await requestGitLabProject(config, repo, `/merge_requests/${pullNumber}`, {
|
|
214
220
|
method: "PUT",
|
|
215
221
|
body
|
|
216
222
|
});
|
|
@@ -219,9 +225,9 @@ export function createGitLabClient(config) {
|
|
|
219
225
|
|
|
220
226
|
async markPullRequestReady(repoUrl, pullNumber) {
|
|
221
227
|
const repo = parseGitLabRepoUrl(repoUrl);
|
|
222
|
-
const current = await
|
|
228
|
+
const current = await requestGitLabProject(config, repo, `/merge_requests/${pullNumber}`);
|
|
223
229
|
const title = stripDraftPrefix(current.title ?? "");
|
|
224
|
-
const payload = await
|
|
230
|
+
const payload = await requestGitLabProject(config, repo, `/merge_requests/${pullNumber}`, {
|
|
225
231
|
method: "PUT",
|
|
226
232
|
body: { title }
|
|
227
233
|
});
|
|
@@ -230,7 +236,7 @@ export function createGitLabClient(config) {
|
|
|
230
236
|
|
|
231
237
|
async findPullRequestByHead(repoUrl, headBranch) {
|
|
232
238
|
const repo = parseGitLabRepoUrl(repoUrl);
|
|
233
|
-
const payload = await
|
|
239
|
+
const payload = await requestGitLabProject(config, repo, "/merge_requests", {
|
|
234
240
|
searchParams: {
|
|
235
241
|
state: "all",
|
|
236
242
|
source_branch: headBranch,
|
|
@@ -248,38 +254,58 @@ export function createGitLabClient(config) {
|
|
|
248
254
|
|
|
249
255
|
async getDefaultBranch(repoUrl) {
|
|
250
256
|
const repo = parseGitLabRepoUrl(repoUrl);
|
|
251
|
-
const payload = await
|
|
257
|
+
const payload = await requestGitLabProject(config, repo);
|
|
252
258
|
return payload.default_branch;
|
|
253
259
|
},
|
|
254
260
|
|
|
255
|
-
async listPrsNeedingReview() {
|
|
256
|
-
const
|
|
257
|
-
if (!
|
|
261
|
+
async listPrsNeedingReview(options = {}) {
|
|
262
|
+
const repoUrls = [...new Set((options.repoUrls || []).map((value) => String(value || "").trim()).filter(Boolean))];
|
|
263
|
+
if (repoUrls.length === 0 && !String(config.GITLAB_BASE_URL || "").trim()) {
|
|
258
264
|
return [];
|
|
259
265
|
}
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
+
const baseUrls = repoUrls.length > 0
|
|
267
|
+
? [...new Set(repoUrls.map((repoUrl) => resolveGitLabBaseUrl(config, repoUrl)))]
|
|
268
|
+
: [resolveGitLabBaseUrl(config)];
|
|
269
|
+
|
|
270
|
+
const results = [];
|
|
271
|
+
|
|
272
|
+
for (const baseUrl of baseUrls) {
|
|
273
|
+
if (!baseUrl) {
|
|
274
|
+
continue;
|
|
266
275
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
|
|
277
|
+
const currentUser = await this.fetchCurrentUser(baseUrl);
|
|
278
|
+
if (!currentUser.login) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const payload = await requestGitLab(config, "/api/v4/merge_requests", {
|
|
282
|
+
baseUrl,
|
|
283
|
+
searchParams: {
|
|
284
|
+
state: "opened",
|
|
285
|
+
reviewer_username: currentUser.login,
|
|
286
|
+
scope: "all",
|
|
287
|
+
per_page: "100"
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
if (!Array.isArray(payload)) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
results.push(...payload
|
|
294
|
+
.filter((mergeRequest) => !Array.isArray(mergeRequest.labels) || !mergeRequest.labels.includes("AI-reviewed"))
|
|
295
|
+
.map((mergeRequest) => ({
|
|
296
|
+
repoUrl: String(mergeRequest.web_url || "").replace(/\/-\/merge_requests\/\d+$/u, ""),
|
|
297
|
+
number: mergeRequest.iid,
|
|
298
|
+
title: mergeRequest.title ?? ""
|
|
299
|
+
}))
|
|
300
|
+
.filter((mergeRequest) => Boolean(mergeRequest.repoUrl)));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return results;
|
|
278
304
|
},
|
|
279
305
|
|
|
280
306
|
async addLabelsToPullRequest(repoUrl, prNumber, labels) {
|
|
281
307
|
const repo = parseGitLabRepoUrl(repoUrl);
|
|
282
|
-
return
|
|
308
|
+
return requestGitLabProject(config, repo, `/merge_requests/${prNumber}`, {
|
|
283
309
|
method: "PUT",
|
|
284
310
|
body: {
|
|
285
311
|
add_labels: labels.join(",")
|
|
@@ -290,7 +316,7 @@ export function createGitLabClient(config) {
|
|
|
290
316
|
async createPullRequestReview(repoUrl, prNumber, body, suggestions) {
|
|
291
317
|
const repo = parseGitLabRepoUrl(repoUrl);
|
|
292
318
|
const reviewBody = buildGitLabReviewBody(body, suggestions);
|
|
293
|
-
return
|
|
319
|
+
return requestGitLabProject(config, repo, `/merge_requests/${prNumber}/notes`, {
|
|
294
320
|
method: "POST",
|
|
295
321
|
body: {
|
|
296
322
|
body: reviewBody
|
|
@@ -300,13 +326,13 @@ export function createGitLabClient(config) {
|
|
|
300
326
|
|
|
301
327
|
async fetchPullRequestDiff(repoUrl, prNumber) {
|
|
302
328
|
const repo = parseGitLabRepoUrl(repoUrl);
|
|
303
|
-
const payload = await
|
|
329
|
+
const payload = await requestGitLabProject(config, repo, `/merge_requests/${prNumber}/changes`);
|
|
304
330
|
return buildGitLabDiff(payload.changes || []);
|
|
305
331
|
},
|
|
306
332
|
|
|
307
333
|
async createPullRequest(repoUrl, pullRequest) {
|
|
308
334
|
const repo = parseGitLabRepoUrl(repoUrl);
|
|
309
|
-
const payload = await
|
|
335
|
+
const payload = await requestGitLabProject(config, repo, "/merge_requests", {
|
|
310
336
|
method: "POST",
|
|
311
337
|
body: {
|
|
312
338
|
title: pullRequest.draft === true ? ensureDraftTitle(pullRequest.title) : pullRequest.title,
|
|
@@ -339,6 +365,13 @@ function issueNotePath(issueNumber) {
|
|
|
339
365
|
return `/issues/${issueNumber}/notes`;
|
|
340
366
|
}
|
|
341
367
|
|
|
368
|
+
function requestGitLabProject(config, repo, suffix = "", init = {}) {
|
|
369
|
+
return requestGitLab(config, projectPath(repo, suffix), {
|
|
370
|
+
...init,
|
|
371
|
+
baseUrl: `${repo.origin}/`
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
342
375
|
function buildGitLabReviewBody(body, suggestions = []) {
|
|
343
376
|
const base = [String(body || "").trim()];
|
|
344
377
|
if (Array.isArray(suggestions) && suggestions.length > 0) {
|
|
@@ -462,7 +495,7 @@ function mapReaction(content) {
|
|
|
462
495
|
async function requestGitLab(config, pathOrUrl, init = {}) {
|
|
463
496
|
const url = pathOrUrl.startsWith("http")
|
|
464
497
|
? new URL(pathOrUrl)
|
|
465
|
-
: new URL(pathOrUrl, determineGitLabBaseUrl(config));
|
|
498
|
+
: new URL(pathOrUrl, init.baseUrl || determineGitLabBaseUrl(config));
|
|
466
499
|
|
|
467
500
|
if (init.searchParams) {
|
|
468
501
|
for (const [key, value] of Object.entries(init.searchParams)) {
|
|
@@ -503,6 +536,30 @@ function determineGitLabBaseUrl(config) {
|
|
|
503
536
|
return "https://gitlab.com/";
|
|
504
537
|
}
|
|
505
538
|
|
|
539
|
+
function resolveGitLabBaseUrl(config, repoUrl = "") {
|
|
540
|
+
const configured = String(config.GITLAB_BASE_URL || "").trim();
|
|
541
|
+
if (configured) {
|
|
542
|
+
return configured.endsWith("/") ? configured : `${configured}/`;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const repoValue = String(repoUrl || "").trim();
|
|
546
|
+
if (!repoValue) {
|
|
547
|
+
return "";
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
try {
|
|
551
|
+
const url = new URL(repoValue);
|
|
552
|
+
if (url.pathname === "/" || url.pathname === "") {
|
|
553
|
+
return `${url.origin}/`;
|
|
554
|
+
}
|
|
555
|
+
} catch {
|
|
556
|
+
// Fall through to repo URL parsing below.
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const repo = parseGitLabRepoUrl(repoValue);
|
|
560
|
+
return `${repo.origin}/`;
|
|
561
|
+
}
|
|
562
|
+
|
|
506
563
|
function buildGitLabRequestError(status, url, body) {
|
|
507
564
|
const detail = String(body || "").slice(0, 300);
|
|
508
565
|
return `GitLab request ${url.pathname}${url.search} failed with ${status}: ${detail}`;
|
package/src/forge/index.js
CHANGED
|
@@ -1,47 +1,76 @@
|
|
|
1
1
|
import { createGitHubClient } from "./github.js";
|
|
2
2
|
import { createGitLabClient } from "./gitlab.js";
|
|
3
3
|
import { getProviderLabel, parseRepoUrl } from "./repo-host.js";
|
|
4
|
+
import { normalizeForgeProvider } from "../config.js";
|
|
4
5
|
|
|
5
6
|
export function createForgeRegistry(config) {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
if (String(config.GITHUB_PAT || "").trim()) {
|
|
9
|
-
clients.set("github", createGitHubClient(config));
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
if (String(config.GITLAB_TOKEN || "").trim()) {
|
|
13
|
-
clients.set("gitlab", createGitLabClient(config));
|
|
14
|
-
}
|
|
7
|
+
const providerId = getActiveForgeProvider(config);
|
|
8
|
+
const client = createClient(providerId, config);
|
|
15
9
|
|
|
16
10
|
return {
|
|
17
11
|
hasClients() {
|
|
18
|
-
return
|
|
12
|
+
return Boolean(client);
|
|
19
13
|
},
|
|
20
14
|
|
|
21
|
-
|
|
22
|
-
return
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
getActiveProviderId() {
|
|
16
|
+
return providerId;
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
getActiveClient() {
|
|
20
|
+
return client
|
|
21
|
+
? {
|
|
22
|
+
id: providerId,
|
|
23
|
+
label: client.label || getProviderLabel(providerId),
|
|
24
|
+
client
|
|
25
|
+
}
|
|
26
|
+
: null;
|
|
27
27
|
},
|
|
28
28
|
|
|
29
29
|
getClientForRepo(repoUrl) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (!client) {
|
|
30
|
+
if (!client || !providerId) {
|
|
31
|
+
const repo = parseRepoUrl(repoUrl);
|
|
33
32
|
throw new Error(`${getProviderLabel(repo.provider)} token is not configured for ${repo.host}.`);
|
|
34
33
|
}
|
|
34
|
+
|
|
35
|
+
const repo = parseRepoUrl(repoUrl);
|
|
36
|
+
if (repo.provider !== providerId) {
|
|
37
|
+
throw new Error(`Active forge is ${getProviderLabel(providerId)}, but repo uses ${getProviderLabel(repo.provider)}.`);
|
|
38
|
+
}
|
|
35
39
|
return client;
|
|
36
40
|
},
|
|
37
41
|
|
|
38
|
-
async listPrsNeedingReview() {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const items = await client.listPrsNeedingReview();
|
|
42
|
-
results.push(...items);
|
|
42
|
+
async listPrsNeedingReview(options = {}) {
|
|
43
|
+
if (!client) {
|
|
44
|
+
return [];
|
|
43
45
|
}
|
|
44
|
-
return
|
|
46
|
+
return client.listPrsNeedingReview(options);
|
|
45
47
|
}
|
|
46
48
|
};
|
|
47
49
|
}
|
|
50
|
+
|
|
51
|
+
export function getActiveForgeProvider(config) {
|
|
52
|
+
const explicit = normalizeForgeProvider(config.FORGE_PROVIDER);
|
|
53
|
+
if (explicit === "gitlab" && String(config.GITLAB_TOKEN || "").trim()) {
|
|
54
|
+
return "gitlab";
|
|
55
|
+
}
|
|
56
|
+
if (explicit === "github" && String(config.GITHUB_PAT || "").trim()) {
|
|
57
|
+
return "github";
|
|
58
|
+
}
|
|
59
|
+
if (String(config.GITLAB_TOKEN || "").trim()) {
|
|
60
|
+
return "gitlab";
|
|
61
|
+
}
|
|
62
|
+
if (String(config.GITHUB_PAT || "").trim()) {
|
|
63
|
+
return "github";
|
|
64
|
+
}
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createClient(providerId, config) {
|
|
69
|
+
if (providerId === "gitlab") {
|
|
70
|
+
return createGitLabClient(config);
|
|
71
|
+
}
|
|
72
|
+
if (providerId === "github") {
|
|
73
|
+
return createGitHubClient(config);
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|