buddy-workbench 0.1.42 → 0.1.44
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/routes/file-organizer.js +5 -1
- package/server/routes/jira-filters.js +22 -2
- package/server/routes/pr-review.js +27 -14
- package/server/routes/settings.js +9 -1
- package/server/routes/updates.js +14 -2
- package/server/services/dialog.js +39 -0
- package/ui/dist/assets/index-Biojo7uc.js +547 -0
- package/ui/dist/assets/index-DOhZFVAd.css +1 -0
- package/ui/dist/index.html +21 -7
- package/ui/dist/sw.js +2 -2
- package/ui/dist/assets/index-43tnCAgi.css +0 -1
- package/ui/dist/assets/index-CE_oDDqW.js +0 -547
package/package.json
CHANGED
|
@@ -91,11 +91,15 @@ router.post('/browse', async (req, res) => {
|
|
|
91
91
|
name: e.name,
|
|
92
92
|
fullPath: path.join(dirPath, e.name)
|
|
93
93
|
}));
|
|
94
|
+
const files = entries
|
|
95
|
+
.filter(e => e.isFile() && !e.name.startsWith('.'))
|
|
96
|
+
.map(e => ({ name: e.name, fullPath: path.join(dirPath, e.name) }));
|
|
94
97
|
|
|
95
98
|
res.json({
|
|
96
99
|
currentPath: dirPath,
|
|
97
100
|
parentPath: path.dirname(dirPath),
|
|
98
|
-
subdirectories: subdirs
|
|
101
|
+
subdirectories: subdirs,
|
|
102
|
+
files
|
|
99
103
|
});
|
|
100
104
|
} catch (err) {
|
|
101
105
|
res.status(500).json({ error: err.message });
|
|
@@ -270,6 +270,7 @@ router.post('/issues/clone', async (req, res) => {
|
|
|
270
270
|
// Fetch original issue details to preserve issue type, project, description, priority
|
|
271
271
|
let origIssueType = null;
|
|
272
272
|
let origProjectKey = null;
|
|
273
|
+
let origSummary = null;
|
|
273
274
|
let origDescription = null;
|
|
274
275
|
let origPriority = null;
|
|
275
276
|
|
|
@@ -289,6 +290,9 @@ router.post('/issues/clone', async (req, res) => {
|
|
|
289
290
|
if (fields.project?.key) {
|
|
290
291
|
origProjectKey = fields.project.key;
|
|
291
292
|
}
|
|
293
|
+
if (fields.summary) {
|
|
294
|
+
origSummary = String(fields.summary).trim();
|
|
295
|
+
}
|
|
292
296
|
if (fields.description) {
|
|
293
297
|
origDescription = fields.description;
|
|
294
298
|
}
|
|
@@ -308,10 +312,12 @@ router.post('/issues/clone', async (req, res) => {
|
|
|
308
312
|
// Determine issue type fallback: fetched original issueType -> passed issueType -> Task
|
|
309
313
|
const targetIssueType = origIssueType || (issueType ? { name: issueType } : { name: 'Task' });
|
|
310
314
|
const projectKey = origProjectKey || (issueKey ? issueKey.split('-')[0] : '');
|
|
315
|
+
const requestedSummary = summary.trim();
|
|
316
|
+
const cloneSummary = origSummary || requestedSummary;
|
|
311
317
|
const url = `https://${jiraHost}/rest/api/2/issue`;
|
|
312
318
|
const payload = {
|
|
313
319
|
fields: {
|
|
314
|
-
summary:
|
|
320
|
+
summary: cloneSummary,
|
|
315
321
|
...(projectKey ? { project: { key: projectKey } } : {}),
|
|
316
322
|
issuetype: targetIssueType,
|
|
317
323
|
...(assigneeField ? { assignee: assigneeField } : {}),
|
|
@@ -325,7 +331,7 @@ router.post('/issues/clone', async (req, res) => {
|
|
|
325
331
|
if (response.status !== 201 && response.status !== 200 && (origPriority || origDescription)) {
|
|
326
332
|
const fallbackPayload = {
|
|
327
333
|
fields: {
|
|
328
|
-
summary:
|
|
334
|
+
summary: cloneSummary,
|
|
329
335
|
...(projectKey ? { project: { key: projectKey } } : {}),
|
|
330
336
|
issuetype: targetIssueType,
|
|
331
337
|
...(assigneeField ? { assignee: assigneeField } : {})
|
|
@@ -355,6 +361,20 @@ router.post('/issues/clone', async (req, res) => {
|
|
|
355
361
|
}
|
|
356
362
|
}
|
|
357
363
|
|
|
364
|
+
// Clone with the original summary first, then apply the requested summary.
|
|
365
|
+
// This keeps both Clone Ticket and + Jira on the same clone-then-edit path.
|
|
366
|
+
if (createdKey && requestedSummary !== cloneSummary) {
|
|
367
|
+
const updateUrl = `https://${jiraHost}/rest/api/2/issue/${encodeURIComponent(createdKey)}`;
|
|
368
|
+
const updateResponse = await httpClient.put(updateUrl, { fields: { summary: requestedSummary } }, { headers });
|
|
369
|
+
if (updateResponse.status < 200 || updateResponse.status >= 300) {
|
|
370
|
+
const errMsg = updateResponse.data?.errorMessages?.[0] ||
|
|
371
|
+
(updateResponse.data?.errors ? Object.values(updateResponse.data.errors).join(', ') : null) ||
|
|
372
|
+
updateResponse.data?.message ||
|
|
373
|
+
`Jira summary update returned status ${updateResponse.status}`;
|
|
374
|
+
return res.status(updateResponse.status).json({ error: errMsg, createdKey });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
358
378
|
const issueUrl = `https://${jiraHost}/browse/${createdKey}`;
|
|
359
379
|
if (trackRecent) {
|
|
360
380
|
const recentIssue = {
|
|
@@ -72,9 +72,9 @@ const analyzeDiff = (filePath, hunks) => {
|
|
|
72
72
|
let segmentText = '';
|
|
73
73
|
const charToLineMap = [];
|
|
74
74
|
|
|
75
|
-
for (const
|
|
76
|
-
const text = line.text
|
|
77
|
-
const lineNum =
|
|
75
|
+
for (const lineObj of lines) {
|
|
76
|
+
const text = lineObj.line ?? lineObj.text ?? '';
|
|
77
|
+
const lineNum = lineObj.destination ?? lineObj.destinationLine ?? 1;
|
|
78
78
|
const startIdx = segmentText.length;
|
|
79
79
|
segmentText += text + '\n';
|
|
80
80
|
const endIdx = segmentText.length;
|
|
@@ -87,7 +87,9 @@ const analyzeDiff = (filePath, hunks) => {
|
|
|
87
87
|
return { lineNum: item.lineNum, text: item.text };
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
-
|
|
90
|
+
const firstText = lines[0]?.line ?? lines[0]?.text ?? '';
|
|
91
|
+
const firstLine = lines[0]?.destination ?? lines[0]?.destinationLine ?? 1;
|
|
92
|
+
return { lineNum: firstLine, text: firstText };
|
|
91
93
|
};
|
|
92
94
|
|
|
93
95
|
const addIssue = (severity, rule, message, charIndex, customCode) => {
|
|
@@ -104,10 +106,10 @@ const analyzeDiff = (filePath, hunks) => {
|
|
|
104
106
|
};
|
|
105
107
|
|
|
106
108
|
// Rule 1: Object property extraction requires lodash.get
|
|
107
|
-
const propAccessRegex = /\b([a-zA-Z_$][\w$]*)\s
|
|
109
|
+
const propAccessRegex = /\b([a-zA-Z_$][\w$]*)\s*(\?\.|\.)\s*([a-zA-Z_$][\w$]*)\b(?!\s*\()/g;
|
|
108
110
|
for (const match of segmentText.matchAll(propAccessRegex)) {
|
|
109
111
|
const objName = match[1];
|
|
110
|
-
const propName = match[
|
|
112
|
+
const propName = match[3];
|
|
111
113
|
const matchIdx = match.index;
|
|
112
114
|
|
|
113
115
|
if (objName === 'row' && propName === 'original') continue;
|
|
@@ -116,7 +118,8 @@ const analyzeDiff = (filePath, hunks) => {
|
|
|
116
118
|
const globalNamespaces = [
|
|
117
119
|
'Math', 'Object', 'Array', 'JSON', 'Promise', 'Reflect', 'Symbol', 'Date',
|
|
118
120
|
'Number', 'String', 'Boolean', 'React', 'process', 'e', 'evt', 'event',
|
|
119
|
-
'console', 'window', 'document', 'history', 'location', 'styles', 'style'
|
|
121
|
+
'console', 'window', 'document', 'history', 'location', 'styles', 'style',
|
|
122
|
+
'req', 'res'
|
|
120
123
|
];
|
|
121
124
|
if (globalNamespaces.includes(objName)) continue;
|
|
122
125
|
|
|
@@ -366,7 +369,7 @@ router.post('/check', async (req, res) => {
|
|
|
366
369
|
|
|
367
370
|
try {
|
|
368
371
|
// 1. Fetch Pull Request details to show basic info
|
|
369
|
-
const prUrl = `https://${host}/rest/api/
|
|
372
|
+
const prUrl = `https://${host}/rest/api/latest/projects/${projectKey}/repos/${repositorySlug}/pull-requests/${pullRequestId}`;
|
|
370
373
|
const prRes = await httpClient.get(prUrl, { headers });
|
|
371
374
|
if (prRes.status < 200 || prRes.status >= 300) {
|
|
372
375
|
if (prRes.status === 401) {
|
|
@@ -377,7 +380,7 @@ router.post('/check', async (req, res) => {
|
|
|
377
380
|
const prInfo = prRes.data;
|
|
378
381
|
|
|
379
382
|
// 2. Fetch changes
|
|
380
|
-
const changesUrl = `https://${host}/rest/api/
|
|
383
|
+
const changesUrl = `https://${host}/rest/api/latest/projects/${projectKey}/repos/${repositorySlug}/pull-requests/${pullRequestId}/changes?limit=1000`;
|
|
381
384
|
const changesRes = await httpClient.get(changesUrl, { headers });
|
|
382
385
|
if (changesRes.status < 200 || changesRes.status >= 300) {
|
|
383
386
|
return res.status(changesRes.status).json({ error: `Failed to fetch PR changes: ${changesRes.statusText || changesRes.status}`, targetUrl: changesUrl });
|
|
@@ -404,14 +407,24 @@ router.post('/check', async (req, res) => {
|
|
|
404
407
|
let warningCount = 0;
|
|
405
408
|
let infoCount = 0;
|
|
406
409
|
|
|
410
|
+
const untilCommit = prInfo.fromRef?.latestCommit || prInfo.fromRef?.id || 'HEAD';
|
|
411
|
+
const sinceCommit = prInfo.toRef?.latestCommit || prInfo.toRef?.id;
|
|
412
|
+
|
|
407
413
|
for (const change of targetChanges) {
|
|
408
414
|
const filePath = change.path.toString;
|
|
409
|
-
|
|
415
|
+
let diffUrl = `https://${host}/rest/api/latest/projects/${projectKey}/repos/${repositorySlug}/commits/${encodeURIComponent(untilCommit)}/diff/${filePath}?contextLines=10000&whitespace=ignore-all&withComments=false`;
|
|
416
|
+
if (sinceCommit) diffUrl += `&since=${encodeURIComponent(sinceCommit)}`;
|
|
417
|
+
|
|
410
418
|
const diffRes = await httpClient.get(diffUrl, { headers });
|
|
411
419
|
if (diffRes.status < 200 || diffRes.status >= 300) continue; // skip file if diff cannot be retrieved
|
|
412
420
|
|
|
413
421
|
const diffData = diffRes.data;
|
|
414
|
-
|
|
422
|
+
let hunks = diffData.hunks;
|
|
423
|
+
if (!hunks && Array.isArray(diffData.diffs)) {
|
|
424
|
+
hunks = diffData.diffs.flatMap(d => d.hunks || []);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const fileIssues = analyzeDiff(filePath, hunks || []);
|
|
415
428
|
|
|
416
429
|
if (fileIssues.length > 0) {
|
|
417
430
|
reviewFiles.push({
|
|
@@ -486,7 +499,7 @@ router.get('/my-prs', async (req, res) => {
|
|
|
486
499
|
const headers = { 'Accept': 'application/json' };
|
|
487
500
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
488
501
|
|
|
489
|
-
const url = `https://${host}/rest/api/
|
|
502
|
+
const url = `https://${host}/rest/api/latest/dashboard/pull-requests?role=author&state=OPEN&limit=100`;
|
|
490
503
|
try {
|
|
491
504
|
const response = await httpClient.get(url, { headers });
|
|
492
505
|
if (response.status < 200 || response.status >= 300) {
|
|
@@ -520,7 +533,7 @@ router.get('/review-prs', async (req, res) => {
|
|
|
520
533
|
const headers = { 'Accept': 'application/json' };
|
|
521
534
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
522
535
|
|
|
523
|
-
const url = `https://${host}/rest/api/
|
|
536
|
+
const url = `https://${host}/rest/api/latest/dashboard/pull-requests?role=reviewer&state=OPEN&limit=100`;
|
|
524
537
|
try {
|
|
525
538
|
const response = await httpClient.get(url, { headers });
|
|
526
539
|
if (response.status < 200 || response.status >= 300) {
|
|
@@ -554,7 +567,7 @@ router.post('/comment', async (req, res) => {
|
|
|
554
567
|
return res.status(400).json({ error: 'Bitbucket Access Token or Host not configured.' });
|
|
555
568
|
}
|
|
556
569
|
|
|
557
|
-
const url = `https://${host}/rest/api/
|
|
570
|
+
const url = `https://${host}/rest/api/latest/projects/${projectKey}/repos/${repositorySlug}/pull-requests/${pullRequestId}/comments`;
|
|
558
571
|
const headers = {
|
|
559
572
|
'Content-Type': 'application/json',
|
|
560
573
|
'Accept': 'application/json',
|
|
@@ -4,7 +4,7 @@ import { paths } from '../config.js';
|
|
|
4
4
|
import { saveAccessToken, saveClipboardDeduplicateMinutes, saveClipboardEnabled, saveClipboardImageEnabled, saveDefaultBrowser, saveDefaultEditor, saveDomain, saveJiraIssuePrefix, saveTheme, settingsStatus } from '../repositories/settings.js';
|
|
5
5
|
import { getNpmPackageVersions, installNvmVersion, manageGlobalNpmPackage, readDevConfigurations, readNvmConfiguration, saveDevConfigurations, searchNpmPackages } from '../services/dev-configurations.js';
|
|
6
6
|
import { openInSystemBrowser, openInSystemEditor, openInSystemFolder } from '../services/browser.js';
|
|
7
|
-
import { selectDirectory } from '../services/dialog.js';
|
|
7
|
+
import { selectDirectory, selectFile } from '../services/dialog.js';
|
|
8
8
|
|
|
9
9
|
const router = Router();
|
|
10
10
|
|
|
@@ -93,6 +93,14 @@ router.post('/select-directory', async (_req, res) => {
|
|
|
93
93
|
res.status(500).json({ error: error.message });
|
|
94
94
|
}
|
|
95
95
|
});
|
|
96
|
+
router.post('/select-file', async (_req, res) => {
|
|
97
|
+
try {
|
|
98
|
+
const result = await selectFile();
|
|
99
|
+
res.json(result);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
res.status(500).json({ error: error.message });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
96
104
|
router.put('/clipboard-enabled', (req, res) => {
|
|
97
105
|
const { enabled } = req.body || {};
|
|
98
106
|
if (typeof enabled !== 'boolean') return res.status(400).json({ error: 'Enabled must be a boolean.' });
|
package/server/routes/updates.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
1
|
+
import { execFile, spawn } from 'node:child_process';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
@@ -157,7 +157,19 @@ router.post('/self-update', async (_req, res) => {
|
|
|
157
157
|
);
|
|
158
158
|
cachedResult = null;
|
|
159
159
|
cachedAt = 0;
|
|
160
|
-
|
|
160
|
+
const message = stdout.trim() || 'DevBuddy was updated successfully.';
|
|
161
|
+
res.json({ success: true, message: `${message} Restarting DevBuddy…` });
|
|
162
|
+
|
|
163
|
+
// Let the update request finish before replacing the running server process.
|
|
164
|
+
setTimeout(() => {
|
|
165
|
+
const restartProcess = spawn(
|
|
166
|
+
process.execPath,
|
|
167
|
+
[join(root, 'bin', 'devbuddy.js'), 'restart'],
|
|
168
|
+
{ cwd: root, detached: true, stdio: 'ignore', windowsHide: true }
|
|
169
|
+
);
|
|
170
|
+
restartProcess.unref();
|
|
171
|
+
}, 1000);
|
|
172
|
+
return undefined;
|
|
161
173
|
} catch (error) {
|
|
162
174
|
const details = error?.stderr?.trim() || error?.stdout?.trim() || error?.message;
|
|
163
175
|
return res.status(500).json({ error: details || 'DevBuddy could not be updated.' });
|
|
@@ -41,3 +41,42 @@ export async function selectDirectory() {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
|
|
45
|
+
export async function selectFile() {
|
|
46
|
+
if (process.platform === 'darwin') {
|
|
47
|
+
try {
|
|
48
|
+
const { stdout } = await execFileAsync('osascript', [
|
|
49
|
+
'-e',
|
|
50
|
+
'POSIX path of (choose file with prompt "Select SSH Key")'
|
|
51
|
+
]);
|
|
52
|
+
const filePath = stdout.trim();
|
|
53
|
+
return filePath ? { path: filePath, canceled: false } : { path: null, canceled: true };
|
|
54
|
+
} catch {
|
|
55
|
+
return { path: null, canceled: true };
|
|
56
|
+
}
|
|
57
|
+
} else if (process.platform === 'win32') {
|
|
58
|
+
try {
|
|
59
|
+
const psScript = `
|
|
60
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
61
|
+
$dialog = New-Object System.Windows.Forms.OpenFileDialog
|
|
62
|
+
$dialog.Title = "Select SSH Key"
|
|
63
|
+
if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
|
|
64
|
+
Write-Output $dialog.FileName
|
|
65
|
+
}
|
|
66
|
+
`;
|
|
67
|
+
const { stdout } = await execFileAsync('powershell', ['-NoProfile', '-Command', psScript]);
|
|
68
|
+
const filePath = stdout.trim();
|
|
69
|
+
return filePath ? { path: filePath, canceled: false } : { path: null, canceled: true };
|
|
70
|
+
} catch {
|
|
71
|
+
return { path: null, canceled: true };
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
try {
|
|
75
|
+
const { stdout } = await execFileAsync('zenity', ['--file-selection', '--title=Select SSH Key']);
|
|
76
|
+
const filePath = stdout.trim();
|
|
77
|
+
return filePath ? { path: filePath, canceled: false } : { path: null, canceled: true };
|
|
78
|
+
} catch {
|
|
79
|
+
return { path: null, canceled: true };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|