buddy-workbench 0.1.8 → 0.1.10
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 +3 -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/jira-filters.js +160 -0
- package/server/routes/launchers.js +2 -2
- package/server/routes/pr-review.js +32 -28
- package/server/routes/settings.js +23 -1
- package/server/routes/static-pages.js +20 -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 +68 -4
- package/server.js +6 -0
- package/ui/dist/assets/index-BbLFuEqP.js +519 -0
- package/ui/dist/assets/index-BxgOdOl7.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-BHs_wPig.css +0 -1
- package/ui/dist/assets/index-CkXRrM63.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.10",
|
|
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,9 @@ 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'),
|
|
10
13
|
shutdownLog: join(root, 'data', 'shutdown.log'),
|
|
11
14
|
clipboardDir: join(root, 'data', 'clipboard'),
|
|
12
15
|
plugins: join(root, 'plugins'),
|
|
@@ -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,160 @@
|
|
|
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 });
|
|
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
|
+
res.status(500).json({ error: error.message || 'Failed to fetch Jira issues.' });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
export default router;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import { listLaunchers, saveLaunchers } from '../repositories/launchers.js';
|
|
3
|
-
import { runScript, runningScripts, scriptErrorLogs, scriptLogs, stopScript } from '../services/process-manager.js';
|
|
3
|
+
import { runScript, runningErrorCounts, runningScripts, scriptErrorLogs, scriptLogs, stopScript } from '../services/process-manager.js';
|
|
4
4
|
import { readPackageScripts } from '../services/package-scripts.js';
|
|
5
5
|
import { currentGitBranch } from '../services/git.js';
|
|
6
6
|
|
|
@@ -23,7 +23,7 @@ router.get('/', async (_req, res) => {
|
|
|
23
23
|
router.post('/', (req, res) => { const config = validLauncher(req.body); if (!config) return invalid(res); const launcher = { id: crypto.randomUUID(), ...config }; const launchers = listLaunchers(); saveLaunchers([...launchers, launcher]); res.status(201).json(launcher); });
|
|
24
24
|
router.put('/:id', (req, res) => { const config = validLauncher(req.body); if (!config) return invalid(res); const launchers = listLaunchers(); const index = launchers.findIndex((item) => item.id === req.params.id); if (index === -1) return res.status(404).json({ error: 'Configuration not found.' }); const launcher = { id: req.params.id, ...config }; launchers[index] = launcher; saveLaunchers(launchers); res.json(launcher); });
|
|
25
25
|
router.delete('/:id', (req, res) => { const launchers = listLaunchers(); const next = launchers.filter((item) => item.id !== req.params.id); if (next.length === launchers.length) return res.status(404).json({ error: 'Configuration not found.' }); saveLaunchers(next); res.status(204).end(); });
|
|
26
|
-
router.get('/running', (_req, res) => res.json(runningScripts()));
|
|
26
|
+
router.get('/running', (_req, res) => res.json({ running: runningScripts(), errorCounts: runningErrorCounts() }));
|
|
27
27
|
router.post('/:id/stop', (req, res) => { try { stopScript(req.params.id, req.body?.scriptId); res.json({ ok: true }); } catch (error) { res.status(404).json({ error: error.message }); } });
|
|
28
28
|
router.post('/:id/start', (req, res) => {
|
|
29
29
|
const launcher = listLaunchers().find((item) => item.id === req.params.id);
|
|
@@ -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
216
|
return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Access Token in Settings.' });
|
|
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}` });
|
|
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}` });
|
|
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) {
|
|
@@ -323,14 +331,14 @@ router.get('/my-prs', async (req, res) => {
|
|
|
323
331
|
|
|
324
332
|
try {
|
|
325
333
|
const url = `https://${host}/rest/api/1.0/dashboard/pull-requests?role=author&state=OPEN&limit=100`;
|
|
326
|
-
const response = await
|
|
327
|
-
if (
|
|
334
|
+
const response = await httpClient.get(url, { headers });
|
|
335
|
+
if (response.status < 200 || response.status >= 300) {
|
|
328
336
|
if (response.status === 401) {
|
|
329
337
|
return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Token.' });
|
|
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}` });
|
|
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) => {
|
|
@@ -357,14 +365,14 @@ router.get('/review-prs', async (req, res) => {
|
|
|
357
365
|
|
|
358
366
|
try {
|
|
359
367
|
const url = `https://${host}/rest/api/1.0/dashboard/pull-requests?role=reviewer&state=OPEN&limit=100`;
|
|
360
|
-
const response = await
|
|
361
|
-
if (
|
|
368
|
+
const response = await httpClient.get(url, { headers });
|
|
369
|
+
if (response.status < 200 || response.status >= 300) {
|
|
362
370
|
if (response.status === 401) {
|
|
363
371
|
return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Token.' });
|
|
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}` });
|
|
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));
|
|
@@ -410,20 +418,16 @@ 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}`
|
|
423
427
|
});
|
|
424
428
|
}
|
|
425
429
|
|
|
426
|
-
const data =
|
|
430
|
+
const data = response.data;
|
|
427
431
|
res.json({ success: true, data });
|
|
428
432
|
} catch (error) {
|
|
429
433
|
res.status(500).json({ error: error.message });
|
|
@@ -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.' });
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { listStaticPages, saveStaticPages } from '../repositories/static-pages.js';
|
|
3
|
+
|
|
4
|
+
const router = Router();
|
|
5
|
+
|
|
6
|
+
// List static pages
|
|
7
|
+
router.get('/', (_req, res) => {
|
|
8
|
+
res.json(listStaticPages());
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// Update static pages
|
|
12
|
+
router.post('/', (req, res) => {
|
|
13
|
+
if (!Array.isArray(req.body)) {
|
|
14
|
+
return res.status(400).json({ error: 'Expected an array of static pages.' });
|
|
15
|
+
}
|
|
16
|
+
saveStaticPages(req.body);
|
|
17
|
+
res.json(listStaticPages());
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export default router;
|