buddy-workbench 0.1.9 → 0.1.11
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 +2 -1
- package/server/config.js +4 -0
- package/server/repositories/errors.js +44 -0
- package/server/repositories/jira-filters.js +16 -0
- package/server/repositories/settings.js +30 -0
- package/server/repositories/static-pages.js +29 -0
- package/server/repositories/todos.js +16 -0
- package/server/routes/clipboard.js +11 -2
- package/server/routes/errors.js +31 -0
- package/server/routes/jira-filters.js +161 -0
- package/server/routes/pr-review.js +41 -36
- package/server/routes/settings.js +23 -1
- package/server/routes/static-pages.js +82 -0
- package/server/routes/todos.js +177 -0
- package/server/services/browser.js +28 -0
- package/server/services/clipboard-history.js +71 -5
- package/server/services/process-manager.js +8 -0
- package/server.js +58 -2
- package/ui/dist/assets/index-BuBK4DeN.css +1 -0
- package/ui/dist/assets/index-CdpFZDzz.js +521 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-BHs_wPig.css +0 -1
- package/ui/dist/assets/index-ntRr7eTI.js +0 -500
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "buddy-workbench",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"prepublishOnly": "npm run build"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"axios": "^1.7.9",
|
|
23
24
|
"express": "^5.1.0"
|
|
24
25
|
}
|
|
25
26
|
}
|
package/server/config.js
CHANGED
|
@@ -7,6 +7,10 @@ export const paths = {
|
|
|
7
7
|
portHistory: join(root, 'data', 'port-history.json'),
|
|
8
8
|
groupTasks: join(root, 'data', 'group-tasks.json'),
|
|
9
9
|
settings: join(root, 'data', 'settings.json'),
|
|
10
|
+
jiraFilters: join(root, 'data', 'jira-filters.json'),
|
|
11
|
+
todos: join(root, 'data', 'todos.json'),
|
|
12
|
+
staticPages: join(root, 'data', 'static-pages.json'),
|
|
13
|
+
errors: join(root, 'data', 'errors.json'),
|
|
10
14
|
shutdownLog: join(root, 'data', 'shutdown.log'),
|
|
11
15
|
clipboardDir: join(root, 'data', 'clipboard'),
|
|
12
16
|
plugins: join(root, 'plugins'),
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { paths } from '../config.js';
|
|
5
|
+
|
|
6
|
+
export function listErrors() {
|
|
7
|
+
try {
|
|
8
|
+
return existsSync(paths.errors) ? JSON.parse(readFileSync(paths.errors, 'utf8')) : [];
|
|
9
|
+
} catch {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function saveErrors(errors) {
|
|
15
|
+
mkdirSync(dirname(paths.errors), { recursive: true });
|
|
16
|
+
const trimmed = Array.isArray(errors) ? errors.slice(-100) : [];
|
|
17
|
+
writeFileSync(paths.errors, JSON.stringify(trimmed, null, 2));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function addErrorRecord({ source = 'System', message = '', details = '', level = 'error' }) {
|
|
21
|
+
if (!message && !details) return null;
|
|
22
|
+
const current = listErrors();
|
|
23
|
+
const record = {
|
|
24
|
+
id: crypto.randomUUID(),
|
|
25
|
+
timestamp: new Date().toISOString(),
|
|
26
|
+
source: String(source || 'System').trim(),
|
|
27
|
+
level: ['fatal', 'warning', 'error'].includes(level) ? level : 'error',
|
|
28
|
+
message: String(message || '').trim() || 'Unspecified Error',
|
|
29
|
+
details: String(details || '').trim()
|
|
30
|
+
};
|
|
31
|
+
current.push(record);
|
|
32
|
+
saveErrors(current);
|
|
33
|
+
return record;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function clearAllErrors() {
|
|
37
|
+
saveErrors([]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function deleteErrorRecord(id) {
|
|
41
|
+
const current = listErrors();
|
|
42
|
+
const filtered = current.filter((item) => item.id !== id);
|
|
43
|
+
saveErrors(filtered);
|
|
44
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { paths } from '../config.js';
|
|
4
|
+
|
|
5
|
+
export function listJiraFilters() {
|
|
6
|
+
try {
|
|
7
|
+
return existsSync(paths.jiraFilters) ? JSON.parse(readFileSync(paths.jiraFilters, 'utf8')) : [];
|
|
8
|
+
} catch {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function saveJiraFilters(filters) {
|
|
14
|
+
mkdirSync(dirname(paths.jiraFilters), { recursive: true });
|
|
15
|
+
writeFileSync(paths.jiraFilters, JSON.stringify(filters, null, 2));
|
|
16
|
+
}
|
|
@@ -11,6 +11,9 @@ export function settingsStatus() {
|
|
|
11
11
|
const settings = readSettings();
|
|
12
12
|
return {
|
|
13
13
|
domain: typeof settings.domain === 'string' ? settings.domain : '',
|
|
14
|
+
jiraIssuePrefix: typeof settings.jiraIssuePrefix === 'string' ? settings.jiraIssuePrefix : '',
|
|
15
|
+
defaultEditor: typeof settings.defaultEditor === 'string' ? settings.defaultEditor : 'vscode',
|
|
16
|
+
defaultBrowser: typeof settings.defaultBrowser === 'string' ? settings.defaultBrowser : 'chrome',
|
|
14
17
|
bitbucketTokenConfigured: Boolean(settings.bitbucketAccessToken),
|
|
15
18
|
jiraTokenConfigured: Boolean(settings.jiraAccessToken),
|
|
16
19
|
confluenceTokenConfigured: Boolean(settings.confluenceAccessToken),
|
|
@@ -27,6 +30,33 @@ export function saveDomain(domain) {
|
|
|
27
30
|
return settingsStatus();
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
export function saveJiraIssuePrefix(jiraIssuePrefix) {
|
|
34
|
+
const settings = readSettings();
|
|
35
|
+
if (jiraIssuePrefix) settings.jiraIssuePrefix = jiraIssuePrefix;
|
|
36
|
+
else delete settings.jiraIssuePrefix;
|
|
37
|
+
mkdirSync(dirname(paths.settings), { recursive: true });
|
|
38
|
+
writeFileSync(paths.settings, JSON.stringify(settings, null, 2), { mode: 0o600 });
|
|
39
|
+
return settingsStatus();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function saveDefaultEditor(defaultEditor) {
|
|
43
|
+
const settings = readSettings();
|
|
44
|
+
const valid = ['vscode', 'devin', 'idea'];
|
|
45
|
+
settings.defaultEditor = valid.includes(defaultEditor) ? defaultEditor : 'vscode';
|
|
46
|
+
mkdirSync(dirname(paths.settings), { recursive: true });
|
|
47
|
+
writeFileSync(paths.settings, JSON.stringify(settings, null, 2), { mode: 0o600 });
|
|
48
|
+
return settingsStatus();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function saveDefaultBrowser(defaultBrowser) {
|
|
52
|
+
const settings = readSettings();
|
|
53
|
+
const valid = ['chrome', 'edge', 'safari'];
|
|
54
|
+
settings.defaultBrowser = valid.includes(defaultBrowser) ? defaultBrowser : 'chrome';
|
|
55
|
+
mkdirSync(dirname(paths.settings), { recursive: true });
|
|
56
|
+
writeFileSync(paths.settings, JSON.stringify(settings, null, 2), { mode: 0o600 });
|
|
57
|
+
return settingsStatus();
|
|
58
|
+
}
|
|
59
|
+
|
|
30
60
|
export function saveClipboardEnabled(enabled) {
|
|
31
61
|
const settings = readSettings();
|
|
32
62
|
settings.clipboardEnabled = Boolean(enabled);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { paths } from '../config.js';
|
|
4
|
+
|
|
5
|
+
function normalize(item, index) {
|
|
6
|
+
const id = item.id || `static-${index + 1}`;
|
|
7
|
+
return {
|
|
8
|
+
id,
|
|
9
|
+
name: item.name || `Page ${index + 1}`,
|
|
10
|
+
url: item.url || '',
|
|
11
|
+
icon: item.icon || 'GlobalOutlined'
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function listStaticPages() {
|
|
16
|
+
if (!existsSync(paths.staticPages)) return [];
|
|
17
|
+
try {
|
|
18
|
+
const raw = JSON.parse(readFileSync(paths.staticPages, 'utf8'));
|
|
19
|
+
if (!Array.isArray(raw)) return [];
|
|
20
|
+
return raw.map(normalize);
|
|
21
|
+
} catch {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function saveStaticPages(pages) {
|
|
27
|
+
mkdirSync(dirname(paths.staticPages), { recursive: true });
|
|
28
|
+
writeFileSync(paths.staticPages, JSON.stringify(pages, null, 2));
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { paths } from '../config.js';
|
|
4
|
+
|
|
5
|
+
export function listTodos() {
|
|
6
|
+
try {
|
|
7
|
+
return existsSync(paths.todos) ? JSON.parse(readFileSync(paths.todos, 'utf8')) : [];
|
|
8
|
+
} catch {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function saveTodos(todos) {
|
|
14
|
+
mkdirSync(dirname(paths.todos), { recursive: true });
|
|
15
|
+
writeFileSync(paths.todos, JSON.stringify(todos, null, 2));
|
|
16
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import express, { Router } from 'express';
|
|
2
|
-
import { clipboardDates, clipboardItems, clipboardOriginal, deleteClipboardItem, saveClipboardImage, clipboardImagePath } from '../services/clipboard-history.js';
|
|
2
|
+
import { clipboardDates, clipboardItems, clipboardOriginal, deleteClipboardItem, saveClipboardImage, clipboardImagePath, allTaggedClipboardItems, updateClipboardItemTags } from '../services/clipboard-history.js';
|
|
3
3
|
|
|
4
4
|
const router = Router();
|
|
5
5
|
router.get('/', (req, res) => res.json({ dates: clipboardDates(), items: clipboardItems(req.query.date) }));
|
|
6
|
+
router.get('/tagged', (_req, res) => res.json({ items: allTaggedClipboardItems() }));
|
|
6
7
|
|
|
7
8
|
router.post('/image', express.raw({ type: 'image/*', limit: '10mb' }), (req, res) => {
|
|
8
9
|
try {
|
|
@@ -10,7 +11,9 @@ router.post('/image', express.raw({ type: 'image/*', limit: '10mb' }), (req, res
|
|
|
10
11
|
if (!buffer || buffer.length === 0) {
|
|
11
12
|
return res.status(400).json({ error: 'Image body is empty.' });
|
|
12
13
|
}
|
|
13
|
-
const
|
|
14
|
+
const contentType = req.headers['content-type'] || 'image/png';
|
|
15
|
+
const ext = contentType.includes('jpeg') || contentType.includes('jpg') ? 'jpg' : 'png';
|
|
16
|
+
const item = saveClipboardImage(buffer, ext);
|
|
14
17
|
res.json(item);
|
|
15
18
|
} catch (error) {
|
|
16
19
|
res.status(500).json({ error: error.message });
|
|
@@ -27,5 +30,11 @@ router.get('/image/:filename', (req, res) => {
|
|
|
27
30
|
});
|
|
28
31
|
|
|
29
32
|
router.get('/:date/:id', (req, res) => { const text = clipboardOriginal(req.params.date, req.params.id); if (text === null) return res.status(404).json({ error: 'Clipboard entry not found.' }); res.type('text/plain').send(text); });
|
|
33
|
+
router.put('/:date/:id/tags', (req, res) => {
|
|
34
|
+
const { tags } = req.body || {};
|
|
35
|
+
const updated = updateClipboardItemTags(req.params.date, req.params.id, tags);
|
|
36
|
+
if (!updated) return res.status(404).json({ error: 'Clipboard entry not found.' });
|
|
37
|
+
res.json(updated);
|
|
38
|
+
});
|
|
30
39
|
router.delete('/:date/:id', (req, res) => { if (!deleteClipboardItem(req.params.date, req.params.id)) return res.status(404).json({ error: 'Clipboard entry not found.' }); res.status(204).end(); });
|
|
31
40
|
export default router;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { addErrorRecord, clearAllErrors, deleteErrorRecord, listErrors } from '../repositories/errors.js';
|
|
3
|
+
|
|
4
|
+
const router = Router();
|
|
5
|
+
|
|
6
|
+
router.get('/', (_req, res) => {
|
|
7
|
+
const errors = listErrors();
|
|
8
|
+
// Return newest first
|
|
9
|
+
res.json([...errors].reverse());
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
router.post('/', (req, res) => {
|
|
13
|
+
const { source, message, details, level } = req.body || {};
|
|
14
|
+
if (!message && !details) {
|
|
15
|
+
return res.status(400).json({ error: 'Message or details is required.' });
|
|
16
|
+
}
|
|
17
|
+
const record = addErrorRecord({ source, message, details, level });
|
|
18
|
+
res.status(201).json(record);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
router.delete('/', (_req, res) => {
|
|
22
|
+
clearAllErrors();
|
|
23
|
+
res.status(204).end();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
router.delete('/:id', (req, res) => {
|
|
27
|
+
deleteErrorRecord(req.params.id);
|
|
28
|
+
res.status(204).end();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export default router;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import https from 'node:https';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import { Router } from 'express';
|
|
4
|
+
import { readSettings } from '../repositories/settings.js';
|
|
5
|
+
import { listJiraFilters, saveJiraFilters } from '../repositories/jira-filters.js';
|
|
6
|
+
|
|
7
|
+
const router = Router();
|
|
8
|
+
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
9
|
+
const httpClient = axios.create({
|
|
10
|
+
httpsAgent,
|
|
11
|
+
validateStatus: () => true
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
function getJiraHost() {
|
|
15
|
+
const settings = readSettings();
|
|
16
|
+
const domain = settings.domain || '';
|
|
17
|
+
if (!domain) return '';
|
|
18
|
+
const clean = domain.replace(/^https?:\/\//i, '').replace(/\/+$/, '');
|
|
19
|
+
if (clean.includes('jira') || clean.includes('.atlassian.net')) return clean;
|
|
20
|
+
return `jira.${clean}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getPriorityRank(priority) {
|
|
24
|
+
if (!priority) return 0;
|
|
25
|
+
const name = (typeof priority === 'string' ? priority : priority.name || '').toLowerCase();
|
|
26
|
+
if (name.includes('blocker') || name.includes('highest') || name.includes('p0') || name.includes('urgent')) return 50;
|
|
27
|
+
if (name.includes('critical') || name.includes('high') || name.includes('p1')) return 40;
|
|
28
|
+
if (name.includes('major') || name.includes('medium') || name.includes('normal') || name.includes('p2')) return 30;
|
|
29
|
+
if (name.includes('minor') || name.includes('low') || name.includes('p3')) return 20;
|
|
30
|
+
if (name.includes('trivial') || name.includes('lowest') || name.includes('p4')) return 10;
|
|
31
|
+
|
|
32
|
+
if (typeof priority === 'object' && priority.id && !isNaN(Number(priority.id))) {
|
|
33
|
+
return 100 - Number(priority.id);
|
|
34
|
+
}
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// List all filters
|
|
39
|
+
router.get('/', (_req, res) => {
|
|
40
|
+
res.json(listJiraFilters());
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Create filter
|
|
44
|
+
router.post('/', (req, res) => {
|
|
45
|
+
const { name, filterId } = req.body || {};
|
|
46
|
+
if (!name || typeof name !== 'string' || !name.trim()) {
|
|
47
|
+
return res.status(400).json({ error: 'Filter name is required.' });
|
|
48
|
+
}
|
|
49
|
+
if (!filterId || (typeof filterId !== 'string' && typeof filterId !== 'number') || !String(filterId).trim()) {
|
|
50
|
+
return res.status(400).json({ error: 'Filter ID is required.' });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const filters = listJiraFilters();
|
|
54
|
+
const newFilter = {
|
|
55
|
+
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
|
56
|
+
name: name.trim(),
|
|
57
|
+
filterId: String(filterId).trim(),
|
|
58
|
+
createdAt: new Date().toISOString()
|
|
59
|
+
};
|
|
60
|
+
filters.push(newFilter);
|
|
61
|
+
saveJiraFilters(filters);
|
|
62
|
+
res.status(201).json(newFilter);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Update filter
|
|
66
|
+
router.put('/:id', (req, res) => {
|
|
67
|
+
const { id } = req.params;
|
|
68
|
+
const { name, filterId } = req.body || {};
|
|
69
|
+
const filters = listJiraFilters();
|
|
70
|
+
const index = filters.findIndex((f) => f.id === id);
|
|
71
|
+
if (index === -1) {
|
|
72
|
+
return res.status(404).json({ error: 'Filter not found.' });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (name !== undefined) {
|
|
76
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
77
|
+
return res.status(400).json({ error: 'Filter name cannot be empty.' });
|
|
78
|
+
}
|
|
79
|
+
filters[index].name = name.trim();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (filterId !== undefined) {
|
|
83
|
+
if ((typeof filterId !== 'string' && typeof filterId !== 'number') || !String(filterId).trim()) {
|
|
84
|
+
return res.status(400).json({ error: 'Filter ID cannot be empty.' });
|
|
85
|
+
}
|
|
86
|
+
filters[index].filterId = String(filterId).trim();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
filters[index].updatedAt = new Date().toISOString();
|
|
90
|
+
saveJiraFilters(filters);
|
|
91
|
+
res.json(filters[index]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Delete filter
|
|
95
|
+
router.delete('/:id', (req, res) => {
|
|
96
|
+
const { id } = req.params;
|
|
97
|
+
const filters = listJiraFilters();
|
|
98
|
+
const filtered = filters.filter((f) => f.id !== id);
|
|
99
|
+
if (filtered.length === filters.length) {
|
|
100
|
+
return res.status(404).json({ error: 'Filter not found.' });
|
|
101
|
+
}
|
|
102
|
+
saveJiraFilters(filtered);
|
|
103
|
+
res.status(204).end();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Fetch issues for a filter
|
|
107
|
+
router.get('/:id/issues', async (req, res) => {
|
|
108
|
+
const { id } = req.params;
|
|
109
|
+
const filters = listJiraFilters();
|
|
110
|
+
const filter = filters.find((f) => f.id === id);
|
|
111
|
+
if (!filter) {
|
|
112
|
+
return res.status(404).json({ error: 'Filter not found.' });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const jiraHost = getJiraHost();
|
|
116
|
+
if (!jiraHost) {
|
|
117
|
+
return res.status(400).json({ error: 'Jira domain is not configured. Please set Domain in Settings.' });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const settings = readSettings();
|
|
121
|
+
const token = settings.jiraAccessToken;
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const url = `https://${jiraHost}/rest/api/2/search?jql=filter%3D${encodeURIComponent(filter.filterId)}&maxResults=200&fields=summary,priority,duedate,status`;
|
|
125
|
+
const headers = {};
|
|
126
|
+
if (token) {
|
|
127
|
+
headers.Authorization = token.startsWith('Bearer ') ? token : `Bearer ${token}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const response = await httpClient.get(url, { headers });
|
|
131
|
+
if (response.status !== 200) {
|
|
132
|
+
const errMsg = response.data?.errorMessages?.[0] || response.data?.message || `Jira API returned status ${response.status}`;
|
|
133
|
+
return res.status(response.status).json({ error: errMsg, targetUrl: url });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const rawIssues = response.data?.issues || [];
|
|
137
|
+
const issues = rawIssues.map((issue) => {
|
|
138
|
+
const key = issue.key || issue.id;
|
|
139
|
+
const fields = issue.fields || {};
|
|
140
|
+
return {
|
|
141
|
+
id: issue.id,
|
|
142
|
+
key,
|
|
143
|
+
summary: fields.summary || '(No summary)',
|
|
144
|
+
priority: fields.priority || null,
|
|
145
|
+
dueDate: fields.duedate || null,
|
|
146
|
+
status: fields.status?.name || null,
|
|
147
|
+
url: `https://${jiraHost}/browse/${key}`
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Sort by priority descending
|
|
152
|
+
issues.sort((a, b) => getPriorityRank(b.priority) - getPriorityRank(a.priority));
|
|
153
|
+
|
|
154
|
+
res.json({ filter, issues });
|
|
155
|
+
} catch (error) {
|
|
156
|
+
const targetUrl = `https://${jiraHost}/rest/api/2/search?jql=filter%3D${encodeURIComponent(filter.filterId)}&maxResults=200&fields=summary,priority,duedate,status`;
|
|
157
|
+
res.status(500).json({ error: error.message || 'Failed to fetch Jira issues.', targetUrl });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
export default router;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import https from 'node:https';
|
|
2
|
+
import axios from 'axios';
|
|
1
3
|
import { Router } from 'express';
|
|
2
4
|
import { readSettings } from '../repositories/settings.js';
|
|
3
5
|
|
|
@@ -5,6 +7,12 @@ const router = Router();
|
|
|
5
7
|
const previewLimit = 2000;
|
|
6
8
|
let lastUsedHost = '';
|
|
7
9
|
|
|
10
|
+
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
11
|
+
const httpClient = axios.create({
|
|
12
|
+
httpsAgent,
|
|
13
|
+
validateStatus: () => true
|
|
14
|
+
});
|
|
15
|
+
|
|
8
16
|
function getBitbucketHost() {
|
|
9
17
|
if (lastUsedHost) return lastUsedHost;
|
|
10
18
|
const settings = readSettings();
|
|
@@ -202,22 +210,22 @@ router.post('/check', async (req, res) => {
|
|
|
202
210
|
try {
|
|
203
211
|
// 1. Fetch Pull Request details to show basic info
|
|
204
212
|
const prUrl = `https://${host}/rest/api/1.0/projects/${projectKey}/repos/${repositorySlug}/pull-requests/${pullRequestId}`;
|
|
205
|
-
const prRes = await
|
|
206
|
-
if (
|
|
213
|
+
const prRes = await httpClient.get(prUrl, { headers });
|
|
214
|
+
if (prRes.status < 200 || prRes.status >= 300) {
|
|
207
215
|
if (prRes.status === 401) {
|
|
208
|
-
return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Access Token in Settings.' });
|
|
216
|
+
return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Access Token in Settings.', targetUrl: prUrl });
|
|
209
217
|
}
|
|
210
|
-
return res.status(prRes.status).json({ error: `Failed to fetch PR info: ${prRes.statusText}
|
|
218
|
+
return res.status(prRes.status).json({ error: `Failed to fetch PR info: ${prRes.statusText || prRes.status}`, targetUrl: prUrl });
|
|
211
219
|
}
|
|
212
|
-
const prInfo =
|
|
220
|
+
const prInfo = prRes.data;
|
|
213
221
|
|
|
214
222
|
// 2. Fetch changes
|
|
215
223
|
const changesUrl = `https://${host}/rest/api/1.0/projects/${projectKey}/repos/${repositorySlug}/pull-requests/${pullRequestId}/changes?limit=1000`;
|
|
216
|
-
const changesRes = await
|
|
217
|
-
if (
|
|
218
|
-
return res.status(changesRes.status).json({ error: `Failed to fetch PR changes: ${changesRes.statusText}
|
|
224
|
+
const changesRes = await httpClient.get(changesUrl, { headers });
|
|
225
|
+
if (changesRes.status < 200 || changesRes.status >= 300) {
|
|
226
|
+
return res.status(changesRes.status).json({ error: `Failed to fetch PR changes: ${changesRes.statusText || changesRes.status}`, targetUrl: changesUrl });
|
|
219
227
|
}
|
|
220
|
-
const changesData =
|
|
228
|
+
const changesData = changesRes.data;
|
|
221
229
|
|
|
222
230
|
const allChanges = changesData.values || [];
|
|
223
231
|
const filteredOut = [];
|
|
@@ -242,10 +250,10 @@ router.post('/check', async (req, res) => {
|
|
|
242
250
|
for (const change of targetChanges) {
|
|
243
251
|
const filePath = change.path.toString;
|
|
244
252
|
const diffUrl = `https://${host}/rest/api/1.0/projects/${projectKey}/repos/${repositorySlug}/pull-requests/${pullRequestId}/diff/${encodeURIComponent(filePath)}?context=0`;
|
|
245
|
-
const diffRes = await
|
|
246
|
-
if (
|
|
253
|
+
const diffRes = await httpClient.get(diffUrl, { headers });
|
|
254
|
+
if (diffRes.status < 200 || diffRes.status >= 300) continue; // skip file if diff cannot be retrieved
|
|
247
255
|
|
|
248
|
-
const diffData =
|
|
256
|
+
const diffData = diffRes.data;
|
|
249
257
|
const fileIssues = analyzeDiff(filePath, diffData.hunks);
|
|
250
258
|
|
|
251
259
|
if (fileIssues.length > 0) {
|
|
@@ -321,16 +329,16 @@ router.get('/my-prs', async (req, res) => {
|
|
|
321
329
|
const headers = { 'Accept': 'application/json' };
|
|
322
330
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
323
331
|
|
|
332
|
+
const url = `https://${host}/rest/api/1.0/dashboard/pull-requests?role=author&state=OPEN&limit=100`;
|
|
324
333
|
try {
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
if (!response.ok) {
|
|
334
|
+
const response = await httpClient.get(url, { headers });
|
|
335
|
+
if (response.status < 200 || response.status >= 300) {
|
|
328
336
|
if (response.status === 401) {
|
|
329
|
-
return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Token.' });
|
|
337
|
+
return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Token.', targetUrl: url });
|
|
330
338
|
}
|
|
331
|
-
return res.status(response.status).json({ error: `Bitbucket API error: ${response.statusText}
|
|
339
|
+
return res.status(response.status).json({ error: `Bitbucket API error: ${response.statusText || response.status}`, targetUrl: url });
|
|
332
340
|
}
|
|
333
|
-
const data =
|
|
341
|
+
const data = response.data;
|
|
334
342
|
const values = data.values || [];
|
|
335
343
|
|
|
336
344
|
const sorted = values.sort((a, b) => {
|
|
@@ -342,7 +350,7 @@ router.get('/my-prs', async (req, res) => {
|
|
|
342
350
|
|
|
343
351
|
res.json({ values: enrichPrList(sorted, host) });
|
|
344
352
|
} catch (error) {
|
|
345
|
-
res.status(500).json({ error: error.message });
|
|
353
|
+
res.status(500).json({ error: error.message, targetUrl: url });
|
|
346
354
|
}
|
|
347
355
|
});
|
|
348
356
|
|
|
@@ -355,23 +363,23 @@ router.get('/review-prs', async (req, res) => {
|
|
|
355
363
|
const headers = { 'Accept': 'application/json' };
|
|
356
364
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
357
365
|
|
|
366
|
+
const url = `https://${host}/rest/api/1.0/dashboard/pull-requests?role=reviewer&state=OPEN&limit=100`;
|
|
358
367
|
try {
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
if (!response.ok) {
|
|
368
|
+
const response = await httpClient.get(url, { headers });
|
|
369
|
+
if (response.status < 200 || response.status >= 300) {
|
|
362
370
|
if (response.status === 401) {
|
|
363
|
-
return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Token.' });
|
|
371
|
+
return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Token.', targetUrl: url });
|
|
364
372
|
}
|
|
365
|
-
return res.status(response.status).json({ error: `Bitbucket API error: ${response.statusText}
|
|
373
|
+
return res.status(response.status).json({ error: `Bitbucket API error: ${response.statusText || response.status}`, targetUrl: url });
|
|
366
374
|
}
|
|
367
|
-
const data =
|
|
375
|
+
const data = response.data;
|
|
368
376
|
const values = data.values || [];
|
|
369
377
|
|
|
370
378
|
const sorted = values.sort((a, b) => (b.updatedDate || 0) - (a.updatedDate || 0));
|
|
371
379
|
|
|
372
380
|
res.json({ values: enrichPrList(sorted, host) });
|
|
373
381
|
} catch (error) {
|
|
374
|
-
res.status(500).json({ error: error.message });
|
|
382
|
+
res.status(500).json({ error: error.message, targetUrl: url });
|
|
375
383
|
}
|
|
376
384
|
});
|
|
377
385
|
|
|
@@ -410,23 +418,20 @@ router.post('/comment', async (req, res) => {
|
|
|
410
418
|
}
|
|
411
419
|
|
|
412
420
|
try {
|
|
413
|
-
const response = await
|
|
414
|
-
method: 'POST',
|
|
415
|
-
headers,
|
|
416
|
-
body: JSON.stringify(payload)
|
|
417
|
-
});
|
|
421
|
+
const response = await httpClient.post(url, payload, { headers });
|
|
418
422
|
|
|
419
|
-
if (
|
|
420
|
-
const errJson =
|
|
423
|
+
if (response.status < 200 || response.status >= 300) {
|
|
424
|
+
const errJson = response.data || {};
|
|
421
425
|
return res.status(response.status).json({
|
|
422
|
-
error: errJson.errors?.[0]?.message || errJson.message || `Bitbucket API error: ${response.statusText}
|
|
426
|
+
error: errJson.errors?.[0]?.message || errJson.message || `Bitbucket API error: ${response.statusText || response.status}`,
|
|
427
|
+
targetUrl: url
|
|
423
428
|
});
|
|
424
429
|
}
|
|
425
430
|
|
|
426
|
-
const data =
|
|
431
|
+
const data = response.data;
|
|
427
432
|
res.json({ success: true, data });
|
|
428
433
|
} catch (error) {
|
|
429
|
-
res.status(500).json({ error: error.message });
|
|
434
|
+
res.status(500).json({ error: error.message, targetUrl: url });
|
|
430
435
|
}
|
|
431
436
|
});
|
|
432
437
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
-
import { saveAccessToken, saveClipboardEnabled, saveDomain, settingsStatus } from '../repositories/settings.js';
|
|
2
|
+
import { saveAccessToken, saveClipboardEnabled, saveDefaultBrowser, saveDefaultEditor, saveDomain, saveJiraIssuePrefix, settingsStatus } from '../repositories/settings.js';
|
|
3
|
+
import { openInSystemBrowser } from '../services/browser.js';
|
|
3
4
|
|
|
4
5
|
const router = Router();
|
|
5
6
|
|
|
@@ -9,6 +10,27 @@ router.put('/domain', (req, res) => {
|
|
|
9
10
|
if (typeof domain !== 'string') return res.status(400).json({ error: 'Domain must be a string.' });
|
|
10
11
|
res.json(saveDomain(domain.trim()));
|
|
11
12
|
});
|
|
13
|
+
router.put('/jira-issue-prefix', (req, res) => {
|
|
14
|
+
const { jiraIssuePrefix } = req.body || {};
|
|
15
|
+
if (typeof jiraIssuePrefix !== 'string') return res.status(400).json({ error: 'Jira issue prefix must be a string.' });
|
|
16
|
+
res.json(saveJiraIssuePrefix(jiraIssuePrefix.trim()));
|
|
17
|
+
});
|
|
18
|
+
router.put('/default-editor', (req, res) => {
|
|
19
|
+
const { defaultEditor } = req.body || {};
|
|
20
|
+
if (typeof defaultEditor !== 'string') return res.status(400).json({ error: 'defaultEditor must be a string.' });
|
|
21
|
+
res.json(saveDefaultEditor(defaultEditor.trim()));
|
|
22
|
+
});
|
|
23
|
+
router.put('/default-browser', (req, res) => {
|
|
24
|
+
const { defaultBrowser } = req.body || {};
|
|
25
|
+
if (typeof defaultBrowser !== 'string') return res.status(400).json({ error: 'defaultBrowser must be a string.' });
|
|
26
|
+
res.json(saveDefaultBrowser(defaultBrowser.trim()));
|
|
27
|
+
});
|
|
28
|
+
router.post('/open-url', (req, res) => {
|
|
29
|
+
const { url } = req.body || {};
|
|
30
|
+
if (typeof url !== 'string' || !url) return res.status(400).json({ error: 'URL must be a non-empty string.' });
|
|
31
|
+
openInSystemBrowser(url.trim());
|
|
32
|
+
res.json({ success: true });
|
|
33
|
+
});
|
|
12
34
|
router.put('/clipboard-enabled', (req, res) => {
|
|
13
35
|
const { enabled } = req.body || {};
|
|
14
36
|
if (typeof enabled !== 'boolean') return res.status(400).json({ error: 'Enabled must be a boolean.' });
|