buddy-workbench 0.1.73 → 0.1.75
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/server/repositories/settings.js +22 -2
- package/server/routes/branch-sync.js +1 -1
- package/server/routes/pr-conflicts.js +110 -17
- package/server/routes/pr-review.js +466 -27
- package/server/routes/settings.js +15 -1
- package/ui/dist/assets/index-5ctU62nD.css +1 -0
- package/ui/dist/assets/{index-CXkq0RVQ.js → index-Co1mqqSw.js} +163 -151
- package/ui/dist/index.html +2 -2
- package/ui/dist/sw.js +2 -2
- package/ui/dist/assets/index-CZUbxhFl.css +0 -1
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ export function settingsStatus() {
|
|
|
11
11
|
const settings = readSettings();
|
|
12
12
|
return {
|
|
13
13
|
domain: typeof settings.domain === 'string' ? settings.domain : '',
|
|
14
|
+
aiApiHost: typeof settings.aiApiHost === 'string' ? settings.aiApiHost : '',
|
|
14
15
|
jiraIssuePrefix: typeof settings.jiraIssuePrefix === 'string' ? settings.jiraIssuePrefix : '',
|
|
15
16
|
defaultEditor: typeof settings.defaultEditor === 'string' ? settings.defaultEditor : 'vscode',
|
|
16
17
|
defaultBrowser: typeof settings.defaultBrowser === 'string' ? settings.defaultBrowser : 'chrome',
|
|
@@ -26,6 +27,15 @@ export function settingsStatus() {
|
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
export function saveAiApiHost(aiApiHost) {
|
|
31
|
+
const settings = readSettings();
|
|
32
|
+
if (aiApiHost) settings.aiApiHost = aiApiHost;
|
|
33
|
+
else delete settings.aiApiHost;
|
|
34
|
+
mkdirSync(dirname(paths.settings), { recursive: true });
|
|
35
|
+
writeFileSync(paths.settings, JSON.stringify(settings, null, 2), { mode: 0o600 });
|
|
36
|
+
return settingsStatus();
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
export function saveDomain(domain) {
|
|
30
40
|
const settings = readSettings();
|
|
31
41
|
if (domain) settings.domain = domain;
|
|
@@ -112,7 +122,14 @@ export function readPrReviewCustomRules() {
|
|
|
112
122
|
return Array.isArray(settings.prReviewCustomRules) ? settings.prReviewCustomRules : [];
|
|
113
123
|
}
|
|
114
124
|
|
|
115
|
-
export function
|
|
125
|
+
export function readPrReviewFileFilters() {
|
|
126
|
+
const settings = readSettings();
|
|
127
|
+
return Array.isArray(settings.prReviewFileFilters)
|
|
128
|
+
? settings.prReviewFileFilters.filter((pattern) => typeof pattern === 'string' && pattern.trim()).map((pattern) => pattern.trim())
|
|
129
|
+
: [];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function savePrReviewRules(rules, customRules, fileFilters, defaultRules) {
|
|
116
133
|
const settings = readSettings();
|
|
117
134
|
const allowed = new Map(defaultRules.map((rule) => [rule.id, rule]));
|
|
118
135
|
settings.prReviewRules = {};
|
|
@@ -135,9 +152,12 @@ export function savePrReviewRules(rules, customRules, defaultRules) {
|
|
|
135
152
|
enabled: rule.enabled !== false,
|
|
136
153
|
severity: ['critical', 'warning', 'info'].includes(rule.severity) ? rule.severity : 'warning'
|
|
137
154
|
}));
|
|
155
|
+
settings.prReviewFileFilters = [...new Set((Array.isArray(fileFilters) ? fileFilters : [])
|
|
156
|
+
.filter((pattern) => typeof pattern === 'string' && pattern.trim())
|
|
157
|
+
.map((pattern) => pattern.trim().slice(0, 300)))].slice(0, 100);
|
|
138
158
|
mkdirSync(dirname(paths.settings), { recursive: true });
|
|
139
159
|
writeFileSync(paths.settings, JSON.stringify(settings, null, 2), { mode: 0o600 });
|
|
140
|
-
return { rules: readPrReviewRules(defaultRules), customRules: readPrReviewCustomRules() };
|
|
160
|
+
return { rules: readPrReviewRules(defaultRules), customRules: readPrReviewCustomRules(), fileFilters: readPrReviewFileFilters() };
|
|
141
161
|
}
|
|
142
162
|
|
|
143
163
|
const accessTokenFields = {
|
|
@@ -355,7 +355,7 @@ router.post('/create-pr', async (req, res) => {
|
|
|
355
355
|
|
|
356
356
|
const prPayload = {
|
|
357
357
|
title: title || `Merge ${sourceBranch} to ${targetBranch}`,
|
|
358
|
-
description: `Automated Pull Request created
|
|
358
|
+
description: `Automated Pull Request created.\n\nMerging \`${sourceBranch}\` into \`${targetBranch}\`.`,
|
|
359
359
|
fromRef: {
|
|
360
360
|
id: `refs/heads/${sourceBranch}`,
|
|
361
361
|
repository: {
|
|
@@ -87,9 +87,9 @@ async function findWorkspaceRepository(parsed) {
|
|
|
87
87
|
|
|
88
88
|
for (const folder of candidates) {
|
|
89
89
|
const identity = remoteIdentity(await getGitRemoteUrl(folder));
|
|
90
|
-
if (identity.
|
|
90
|
+
if (identity.repositorySlug && parsed.repositorySlug && identity.repositorySlug.toLowerCase() === parsed.repositorySlug.toLowerCase()) return folder;
|
|
91
91
|
}
|
|
92
|
-
throw new Error(`Could not find ${parsed.
|
|
92
|
+
throw new Error(`Could not find ${parsed.repositorySlug} in workspace: ${workspaceDir}`);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
function validateRelativeFilePath(filePath) {
|
|
@@ -188,6 +188,74 @@ router.get('/my-conflicts', async (_req, res) => {
|
|
|
188
188
|
}
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
+
router.post('/check-branch', async (req, res) => {
|
|
192
|
+
const parsed = parsePrUrl(req.body?.prUrl);
|
|
193
|
+
const branchName = typeof req.body?.branchName === 'string' ? req.body.branchName.trim() : '';
|
|
194
|
+
if (!branchName) return res.json({ exists: false });
|
|
195
|
+
if (!parsed) return res.status(400).json({ error: 'Invalid Pull Request URL.' });
|
|
196
|
+
|
|
197
|
+
// 1. Validate ref format
|
|
198
|
+
if (/[\s~^:?*\[\\]|\.\.|\/\.|\/\/|@\{|^\/|\/$|\.lock$/i.test(branchName)) {
|
|
199
|
+
return res.json({ exists: true, error: `Invalid branch name format: ${branchName}` });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 2. Try checking local repo if workspace exists
|
|
203
|
+
try {
|
|
204
|
+
const folder = await findWorkspaceRepository(parsed);
|
|
205
|
+
try {
|
|
206
|
+
await git(folder, ['check-ref-format', '--branch', branchName]);
|
|
207
|
+
} catch {
|
|
208
|
+
return res.json({ exists: true, error: `Invalid branch name: ${branchName}` });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
await git(folder, ['rev-parse', '--verify', `refs/heads/${branchName}`]);
|
|
213
|
+
return res.json({ exists: true, error: `Local branch already exists: ${branchName}` });
|
|
214
|
+
} catch {}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
await git(folder, ['ls-remote', '--exit-code', '--heads', 'origin', branchName]);
|
|
218
|
+
return res.json({ exists: true, error: `Remote branch already exists on origin: ${branchName}` });
|
|
219
|
+
} catch {}
|
|
220
|
+
} catch {
|
|
221
|
+
// If local repo cannot be located, fallback to Bitbucket REST API
|
|
222
|
+
try {
|
|
223
|
+
const branchesUrl = `https://${parsed.host}/rest/api/latest/projects/${encodeURIComponent(parsed.projectKey)}/repos/${encodeURIComponent(parsed.repositorySlug)}/branches?filterText=${encodeURIComponent(branchName)}&limit=10`;
|
|
224
|
+
const response = await client.get(branchesUrl, { headers: authHeaders() });
|
|
225
|
+
if (response.status >= 200 && response.status < 300) {
|
|
226
|
+
const values = Array.isArray(response.data?.values) ? response.data.values : [];
|
|
227
|
+
const found = values.find((b) => (b.displayId || b.id?.replace('refs/heads/', '')) === branchName);
|
|
228
|
+
if (found) {
|
|
229
|
+
return res.json({ exists: true, error: `Remote branch already exists on origin: ${branchName}` });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} catch {}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return res.json({ exists: false });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
async function applyFileChoice(folder, filePath, choiceRef) {
|
|
239
|
+
try {
|
|
240
|
+
await git(folder, ['checkout', choiceRef, '--', filePath]);
|
|
241
|
+
await git(folder, ['add', '--', filePath]);
|
|
242
|
+
} catch {
|
|
243
|
+
const content = await readGitFile(folder, choiceRef, filePath);
|
|
244
|
+
const absolutePath = join(folder, filePath);
|
|
245
|
+
if (content === null) {
|
|
246
|
+
if (existsSync(absolutePath)) rmSync(absolutePath, { force: true, recursive: true });
|
|
247
|
+
try {
|
|
248
|
+
await git(folder, ['rm', '-f', '--', filePath]);
|
|
249
|
+
} catch {
|
|
250
|
+
await git(folder, ['add', '-A', '--', filePath]);
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
writeFileSync(absolutePath, content);
|
|
254
|
+
await git(folder, ['add', '--', filePath]);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
191
259
|
router.post('/resolve', async (req, res) => {
|
|
192
260
|
const parsed = parsePrUrl(req.body?.prUrl);
|
|
193
261
|
const files = Array.isArray(req.body?.files) ? req.body.files : [];
|
|
@@ -228,7 +296,6 @@ router.post('/resolve', async (req, res) => {
|
|
|
228
296
|
const targetRef = `origin/${targetBranch}`;
|
|
229
297
|
await git(folder, ['rev-parse', '--verify', sourceRef]);
|
|
230
298
|
await git(folder, ['rev-parse', '--verify', targetRef]);
|
|
231
|
-
await git(folder, ['checkout', '-B', sourceBranch, sourceRef]);
|
|
232
299
|
|
|
233
300
|
let commitBranch = sourceBranch;
|
|
234
301
|
if (createNewBranch) {
|
|
@@ -241,28 +308,48 @@ router.post('/resolve', async (req, res) => {
|
|
|
241
308
|
await git(folder, ['ls-remote', '--exit-code', '--heads', 'origin', newBranchName]);
|
|
242
309
|
return res.status(409).json({ error: `Remote branch already exists: ${newBranchName}` });
|
|
243
310
|
} catch {}
|
|
244
|
-
|
|
311
|
+
|
|
312
|
+
// Checkout from target branch (b), then create resolution branch
|
|
313
|
+
await git(folder, ['checkout', '-B', newBranchName, targetRef]);
|
|
245
314
|
commitBranch = newBranchName;
|
|
315
|
+
|
|
316
|
+
// Merge source branch (a) into resolution branch
|
|
317
|
+
try {
|
|
318
|
+
await git(folder, ['merge', '--no-commit', '--no-ff', sourceRef]);
|
|
319
|
+
} catch {
|
|
320
|
+
// Merge conflicts are expected and will be resolved per file choices
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
// Direct resolution on source branch (a) by merging target branch (b)
|
|
324
|
+
await git(folder, ['checkout', '-B', sourceBranch, sourceRef]);
|
|
325
|
+
commitBranch = sourceBranch;
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
await git(folder, ['merge', '--no-commit', '--no-ff', targetRef]);
|
|
329
|
+
} catch {
|
|
330
|
+
// Merge conflicts are expected and will be resolved per file choices
|
|
331
|
+
}
|
|
246
332
|
}
|
|
247
333
|
|
|
248
334
|
const resolvedPaths = [];
|
|
249
335
|
for (const file of files) {
|
|
250
336
|
const filePath = validateRelativeFilePath(file.path);
|
|
251
337
|
const choiceRef = choices[file.path] === 'target' ? targetRef : sourceRef;
|
|
252
|
-
|
|
253
|
-
const absolutePath = join(folder, filePath);
|
|
254
|
-
if (content === null) {
|
|
255
|
-
if (choices[file.path] === 'target' && existsSync(absolutePath)) rmSync(absolutePath);
|
|
256
|
-
else if (choices[file.path] === 'source') throw new Error(`File not found on source branch: ${filePath}`);
|
|
257
|
-
} else {
|
|
258
|
-
writeFileSync(absolutePath, content);
|
|
259
|
-
}
|
|
338
|
+
await applyFileChoice(folder, filePath, choiceRef);
|
|
260
339
|
resolvedPaths.push(filePath);
|
|
261
340
|
}
|
|
262
341
|
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
if (
|
|
342
|
+
// Resolve any remaining unmerged paths that might not have been in the files array
|
|
343
|
+
const unmergedStr = String(await git(folder, ['diff', '--name-only', '--diff-filter=U'])).trim();
|
|
344
|
+
if (unmergedStr) {
|
|
345
|
+
const unmergedFiles = unmergedStr.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
346
|
+
for (const unmergedFile of unmergedFiles) {
|
|
347
|
+
const choiceRef = choices[unmergedFile] === 'target' ? targetRef : sourceRef;
|
|
348
|
+
await applyFileChoice(folder, unmergedFile, choiceRef);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Commit merge resolution
|
|
266
353
|
await git(folder, ['commit', '-m', commitMessage]);
|
|
267
354
|
await git(folder, ['push', 'origin', commitBranch]);
|
|
268
355
|
const commitId = String(await git(folder, ['rev-parse', 'HEAD'])).trim();
|
|
@@ -272,7 +359,7 @@ router.post('/resolve', async (req, res) => {
|
|
|
272
359
|
const createPrUrl = `https://${parsed.host}/rest/api/latest/projects/${encodeURIComponent(parsed.projectKey)}/repos/${encodeURIComponent(parsed.repositorySlug)}/pull-requests`;
|
|
273
360
|
const createPrResponse = await client.post(createPrUrl, {
|
|
274
361
|
title: commitMessage,
|
|
275
|
-
description: `Resolution Pull Request created from ${commitBranch}.\n\nThis branch
|
|
362
|
+
description: `Resolution Pull Request created from ${commitBranch}.\n\nThis branch resolves conflicts for Pull Request #${parsed.pullRequestId} (${sourceBranch} → ${targetBranch}).`,
|
|
276
363
|
fromRef: {
|
|
277
364
|
id: `refs/heads/${commitBranch}`,
|
|
278
365
|
repository: { slug: parsed.repositorySlug, project: { key: parsed.projectKey } }
|
|
@@ -300,10 +387,16 @@ router.post('/resolve', async (req, res) => {
|
|
|
300
387
|
prUrl: resolutionPrUrl
|
|
301
388
|
});
|
|
302
389
|
} catch (error) {
|
|
303
|
-
|
|
390
|
+
const errorMsg = (error.stderr ? String(error.stderr).trim() : '') || error.message || String(error);
|
|
391
|
+
res.status(500).json({ error: `Unable to create resolution commit: ${errorMsg}` });
|
|
304
392
|
} finally {
|
|
305
393
|
if (folder) {
|
|
306
394
|
try {
|
|
395
|
+
if (existsSync(join(folder, '.git', 'MERGE_HEAD'))) {
|
|
396
|
+
await git(folder, ['merge', '--abort']).catch(() => {});
|
|
397
|
+
}
|
|
398
|
+
await git(folder, ['reset', '--hard', 'HEAD']).catch(() => {});
|
|
399
|
+
await git(folder, ['clean', '-fd']).catch(() => {});
|
|
307
400
|
if (originalBranch) await git(folder, ['checkout', originalBranch]);
|
|
308
401
|
else if (originalHead) await git(folder, ['checkout', '--detach', originalHead]);
|
|
309
402
|
} catch {}
|