buddy-workbench 0.1.10 โ 0.1.12
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 +3 -2
- package/pages/.gitkeep +1 -0
- package/pages/welcome/index.html +72 -0
- package/server/config.js +3 -0
- package/server/repositories/errors.js +44 -0
- package/server/routes/errors.js +31 -0
- package/server/routes/jira-filters.js +3 -2
- package/server/routes/pr-review.js +14 -13
- package/server/routes/static-pages.js +64 -2
- package/server/services/process-manager.js +37 -11
- package/server.js +55 -2
- package/ui/dist/assets/index-BfTsErqX.js +521 -0
- package/ui/dist/assets/{index-BxgOdOl7.css โ index-BuBK4DeN.css} +1 -1
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-BbLFuEqP.js +0 -519
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "buddy-workbench",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"server.js",
|
|
11
11
|
"server/",
|
|
12
12
|
"ui/dist/",
|
|
13
|
-
"plugins/"
|
|
13
|
+
"plugins/",
|
|
14
|
+
"pages/"
|
|
14
15
|
],
|
|
15
16
|
"scripts": {
|
|
16
17
|
"start": "exec node server.js",
|
package/pages/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Keep pages directory
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Welcome - DevBuddy Static Pages</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #0f172a;
|
|
10
|
+
--card-bg: #1e293b;
|
|
11
|
+
--text: #f8fafc;
|
|
12
|
+
--text-muted: #94a3b8;
|
|
13
|
+
--accent: #38bdf8;
|
|
14
|
+
--border: #334155;
|
|
15
|
+
}
|
|
16
|
+
body {
|
|
17
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
18
|
+
background: var(--bg);
|
|
19
|
+
color: var(--text);
|
|
20
|
+
display: flex;
|
|
21
|
+
justify-content: center;
|
|
22
|
+
align-items: center;
|
|
23
|
+
min-height: 100vh;
|
|
24
|
+
margin: 0;
|
|
25
|
+
padding: 24px;
|
|
26
|
+
box-sizing: border-box;
|
|
27
|
+
}
|
|
28
|
+
.card {
|
|
29
|
+
background: var(--card-bg);
|
|
30
|
+
padding: 32px;
|
|
31
|
+
border-radius: 16px;
|
|
32
|
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
|
|
33
|
+
border: 1px solid var(--border);
|
|
34
|
+
text-align: center;
|
|
35
|
+
max-width: 520px;
|
|
36
|
+
width: 100%;
|
|
37
|
+
}
|
|
38
|
+
.icon {
|
|
39
|
+
font-size: 48px;
|
|
40
|
+
margin-bottom: 16px;
|
|
41
|
+
}
|
|
42
|
+
h1 {
|
|
43
|
+
color: var(--accent);
|
|
44
|
+
margin-top: 0;
|
|
45
|
+
margin-bottom: 12px;
|
|
46
|
+
font-size: 24px;
|
|
47
|
+
}
|
|
48
|
+
p {
|
|
49
|
+
color: var(--text-muted);
|
|
50
|
+
line-height: 1.6;
|
|
51
|
+
margin-bottom: 20px;
|
|
52
|
+
}
|
|
53
|
+
code {
|
|
54
|
+
background: rgba(15, 23, 42, 0.8);
|
|
55
|
+
padding: 4px 8px;
|
|
56
|
+
border-radius: 6px;
|
|
57
|
+
color: #f43f5e;
|
|
58
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
59
|
+
font-size: 0.9em;
|
|
60
|
+
border: 1px solid var(--border);
|
|
61
|
+
}
|
|
62
|
+
</style>
|
|
63
|
+
</head>
|
|
64
|
+
<body>
|
|
65
|
+
<div class="card">
|
|
66
|
+
<div class="icon">๐</div>
|
|
67
|
+
<h1>Static Server Active</h1>
|
|
68
|
+
<p>Place your HTML, CSS, and JS files inside the <code>pages/</code> directory in your workspace.</p>
|
|
69
|
+
<p>Configure this page in Settings > Static Pages with URL <code>/pages/welcome/index.html</code></p>
|
|
70
|
+
</div>
|
|
71
|
+
</body>
|
|
72
|
+
</html>
|
package/server/config.js
CHANGED
|
@@ -10,8 +10,11 @@ export const paths = {
|
|
|
10
10
|
jiraFilters: join(root, 'data', 'jira-filters.json'),
|
|
11
11
|
todos: join(root, 'data', 'todos.json'),
|
|
12
12
|
staticPages: join(root, 'data', 'static-pages.json'),
|
|
13
|
+
errors: join(root, 'data', 'errors.json'),
|
|
13
14
|
shutdownLog: join(root, 'data', 'shutdown.log'),
|
|
14
15
|
clipboardDir: join(root, 'data', 'clipboard'),
|
|
15
16
|
plugins: join(root, 'plugins'),
|
|
17
|
+
pages: join(root, 'pages'),
|
|
18
|
+
dataPages: join(root, 'data', 'pages'),
|
|
16
19
|
ui: join(root, 'ui', 'dist')
|
|
17
20
|
};
|
|
@@ -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,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;
|
|
@@ -130,7 +130,7 @@ router.get('/:id/issues', async (req, res) => {
|
|
|
130
130
|
const response = await httpClient.get(url, { headers });
|
|
131
131
|
if (response.status !== 200) {
|
|
132
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 });
|
|
133
|
+
return res.status(response.status).json({ error: errMsg, targetUrl: url });
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
const rawIssues = response.data?.issues || [];
|
|
@@ -153,7 +153,8 @@ router.get('/:id/issues', async (req, res) => {
|
|
|
153
153
|
|
|
154
154
|
res.json({ filter, issues });
|
|
155
155
|
} catch (error) {
|
|
156
|
-
|
|
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 });
|
|
157
158
|
}
|
|
158
159
|
});
|
|
159
160
|
|
|
@@ -213,9 +213,9 @@ router.post('/check', async (req, res) => {
|
|
|
213
213
|
const prRes = await httpClient.get(prUrl, { headers });
|
|
214
214
|
if (prRes.status < 200 || prRes.status >= 300) {
|
|
215
215
|
if (prRes.status === 401) {
|
|
216
|
-
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 });
|
|
217
217
|
}
|
|
218
|
-
return res.status(prRes.status).json({ error: `Failed to fetch PR info: ${prRes.statusText || prRes.status}
|
|
218
|
+
return res.status(prRes.status).json({ error: `Failed to fetch PR info: ${prRes.statusText || prRes.status}`, targetUrl: prUrl });
|
|
219
219
|
}
|
|
220
220
|
const prInfo = prRes.data;
|
|
221
221
|
|
|
@@ -223,7 +223,7 @@ router.post('/check', async (req, res) => {
|
|
|
223
223
|
const changesUrl = `https://${host}/rest/api/1.0/projects/${projectKey}/repos/${repositorySlug}/pull-requests/${pullRequestId}/changes?limit=1000`;
|
|
224
224
|
const changesRes = await httpClient.get(changesUrl, { headers });
|
|
225
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}
|
|
226
|
+
return res.status(changesRes.status).json({ error: `Failed to fetch PR changes: ${changesRes.statusText || changesRes.status}`, targetUrl: changesUrl });
|
|
227
227
|
}
|
|
228
228
|
const changesData = changesRes.data;
|
|
229
229
|
|
|
@@ -329,14 +329,14 @@ router.get('/my-prs', async (req, res) => {
|
|
|
329
329
|
const headers = { 'Accept': 'application/json' };
|
|
330
330
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
331
331
|
|
|
332
|
+
const url = `https://${host}/rest/api/1.0/dashboard/pull-requests?role=author&state=OPEN&limit=100`;
|
|
332
333
|
try {
|
|
333
|
-
const url = `https://${host}/rest/api/1.0/dashboard/pull-requests?role=author&state=OPEN&limit=100`;
|
|
334
334
|
const response = await httpClient.get(url, { headers });
|
|
335
335
|
if (response.status < 200 || response.status >= 300) {
|
|
336
336
|
if (response.status === 401) {
|
|
337
|
-
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 });
|
|
338
338
|
}
|
|
339
|
-
return res.status(response.status).json({ error: `Bitbucket API error: ${response.statusText || response.status}
|
|
339
|
+
return res.status(response.status).json({ error: `Bitbucket API error: ${response.statusText || response.status}`, targetUrl: url });
|
|
340
340
|
}
|
|
341
341
|
const data = response.data;
|
|
342
342
|
const values = data.values || [];
|
|
@@ -350,7 +350,7 @@ router.get('/my-prs', async (req, res) => {
|
|
|
350
350
|
|
|
351
351
|
res.json({ values: enrichPrList(sorted, host) });
|
|
352
352
|
} catch (error) {
|
|
353
|
-
res.status(500).json({ error: error.message });
|
|
353
|
+
res.status(500).json({ error: error.message, targetUrl: url });
|
|
354
354
|
}
|
|
355
355
|
});
|
|
356
356
|
|
|
@@ -363,14 +363,14 @@ router.get('/review-prs', async (req, res) => {
|
|
|
363
363
|
const headers = { 'Accept': 'application/json' };
|
|
364
364
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
365
365
|
|
|
366
|
+
const url = `https://${host}/rest/api/1.0/dashboard/pull-requests?role=reviewer&state=OPEN&limit=100`;
|
|
366
367
|
try {
|
|
367
|
-
const url = `https://${host}/rest/api/1.0/dashboard/pull-requests?role=reviewer&state=OPEN&limit=100`;
|
|
368
368
|
const response = await httpClient.get(url, { headers });
|
|
369
369
|
if (response.status < 200 || response.status >= 300) {
|
|
370
370
|
if (response.status === 401) {
|
|
371
|
-
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 });
|
|
372
372
|
}
|
|
373
|
-
return res.status(response.status).json({ error: `Bitbucket API error: ${response.statusText || response.status}
|
|
373
|
+
return res.status(response.status).json({ error: `Bitbucket API error: ${response.statusText || response.status}`, targetUrl: url });
|
|
374
374
|
}
|
|
375
375
|
const data = response.data;
|
|
376
376
|
const values = data.values || [];
|
|
@@ -379,7 +379,7 @@ router.get('/review-prs', async (req, res) => {
|
|
|
379
379
|
|
|
380
380
|
res.json({ values: enrichPrList(sorted, host) });
|
|
381
381
|
} catch (error) {
|
|
382
|
-
res.status(500).json({ error: error.message });
|
|
382
|
+
res.status(500).json({ error: error.message, targetUrl: url });
|
|
383
383
|
}
|
|
384
384
|
});
|
|
385
385
|
|
|
@@ -423,14 +423,15 @@ router.post('/comment', async (req, res) => {
|
|
|
423
423
|
if (response.status < 200 || response.status >= 300) {
|
|
424
424
|
const errJson = response.data || {};
|
|
425
425
|
return res.status(response.status).json({
|
|
426
|
-
error: errJson.errors?.[0]?.message || errJson.message || `Bitbucket API error: ${response.statusText || response.status}
|
|
426
|
+
error: errJson.errors?.[0]?.message || errJson.message || `Bitbucket API error: ${response.statusText || response.status}`,
|
|
427
|
+
targetUrl: url
|
|
427
428
|
});
|
|
428
429
|
}
|
|
429
430
|
|
|
430
431
|
const data = response.data;
|
|
431
432
|
res.json({ success: true, data });
|
|
432
433
|
} catch (error) {
|
|
433
|
-
res.status(500).json({ error: error.message });
|
|
434
|
+
res.status(500).json({ error: error.message, targetUrl: url });
|
|
434
435
|
}
|
|
435
436
|
});
|
|
436
437
|
|
|
@@ -8,8 +8,8 @@ router.get('/', (_req, res) => {
|
|
|
8
8
|
res.json(listStaticPages());
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
-
//
|
|
12
|
-
router.
|
|
11
|
+
// Save all static pages
|
|
12
|
+
router.put('/', (req, res) => {
|
|
13
13
|
if (!Array.isArray(req.body)) {
|
|
14
14
|
return res.status(400).json({ error: 'Expected an array of static pages.' });
|
|
15
15
|
}
|
|
@@ -17,4 +17,66 @@ router.post('/', (req, res) => {
|
|
|
17
17
|
res.json(listStaticPages());
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
+
// Create single static page
|
|
21
|
+
router.post('/', (req, res) => {
|
|
22
|
+
const { name, url } = req.body || {};
|
|
23
|
+
if (!name || typeof name !== 'string' || !name.trim()) {
|
|
24
|
+
return res.status(400).json({ error: 'Page name is required.' });
|
|
25
|
+
}
|
|
26
|
+
if (!url || typeof url !== 'string' || !url.trim()) {
|
|
27
|
+
return res.status(400).json({ error: 'URL is required.' });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const pages = listStaticPages();
|
|
31
|
+
const newPage = {
|
|
32
|
+
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
|
33
|
+
name: name.trim(),
|
|
34
|
+
url: url.trim(),
|
|
35
|
+
icon: 'GlobalOutlined'
|
|
36
|
+
};
|
|
37
|
+
pages.push(newPage);
|
|
38
|
+
saveStaticPages(pages);
|
|
39
|
+
res.status(201).json(newPage);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Update single static page
|
|
43
|
+
router.put('/:id', (req, res) => {
|
|
44
|
+
const { id } = req.params;
|
|
45
|
+
const { name, url } = req.body || {};
|
|
46
|
+
const pages = listStaticPages();
|
|
47
|
+
const index = pages.findIndex((p) => String(p.id) === String(id));
|
|
48
|
+
if (index === -1) {
|
|
49
|
+
return res.status(404).json({ error: 'Static page not found.' });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (name !== undefined) {
|
|
53
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
54
|
+
return res.status(400).json({ error: 'Page name cannot be empty.' });
|
|
55
|
+
}
|
|
56
|
+
pages[index].name = name.trim();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (url !== undefined) {
|
|
60
|
+
if (typeof url !== 'string' || !url.trim()) {
|
|
61
|
+
return res.status(400).json({ error: 'URL cannot be empty.' });
|
|
62
|
+
}
|
|
63
|
+
pages[index].url = url.trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
saveStaticPages(pages);
|
|
67
|
+
res.json(pages[index]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Delete static page
|
|
71
|
+
router.delete('/:id', (req, res) => {
|
|
72
|
+
const { id } = req.params;
|
|
73
|
+
const pages = listStaticPages();
|
|
74
|
+
const filtered = pages.filter((p) => String(p.id) !== String(id));
|
|
75
|
+
if (filtered.length === pages.length) {
|
|
76
|
+
return res.status(404).json({ error: 'Static page not found.' });
|
|
77
|
+
}
|
|
78
|
+
saveStaticPages(filtered);
|
|
79
|
+
res.status(204).end();
|
|
80
|
+
});
|
|
81
|
+
|
|
20
82
|
export default router;
|
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import { execFile } from 'node:child_process';
|
|
6
6
|
import { listPortHistory, savePortHistory } from '../repositories/port-history.js';
|
|
7
|
+
import { addErrorRecord } from '../repositories/errors.js';
|
|
7
8
|
|
|
8
9
|
const running = new Map();
|
|
9
10
|
const logs = new Map();
|
|
@@ -14,7 +15,7 @@ const savedPortHistory = new Map(listPortHistory().map((record) => [record.port,
|
|
|
14
15
|
const supervisorPath = fileURLToPath(new URL('./script-supervisor.js', import.meta.url));
|
|
15
16
|
const execFileAsync = promisify(execFile);
|
|
16
17
|
const keyFor = (launcherId, scriptId) => `${launcherId}:${scriptId}`;
|
|
17
|
-
function countErrorBlocks(text, isStderr = false) {
|
|
18
|
+
export function countErrorBlocks(text, isStderr = false) {
|
|
18
19
|
if (!text) return 0;
|
|
19
20
|
const lines = text.split(/\r?\n/);
|
|
20
21
|
let count = 0;
|
|
@@ -27,14 +28,32 @@ function countErrorBlocks(text, isStderr = false) {
|
|
|
27
28
|
continue;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
const cleanLine = trimmed
|
|
32
|
+
.replace(/[\u001b\x1b]\[[0-9;]*[a-zA-ZKk]/g, '')
|
|
33
|
+
.replace(/[\u001b\x1b](?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
|
|
34
|
+
|
|
35
|
+
const isNonErrorLine =
|
|
36
|
+
/<i>|<s>/i.test(cleanLine) ||
|
|
37
|
+
/^\s*<(?:i|s|w|info|success|warn|warning)>/i.test(cleanLine) ||
|
|
38
|
+
/^\s*\[(?:info|success|warn|warning|note|notice)\]/i.test(cleanLine) ||
|
|
39
|
+
/^(?:npm WARN|yarn warning|pnpm warning)/i.test(cleanLine) ||
|
|
40
|
+
/^\s*\((?:node:\d+|node:|Use\b)/i.test(cleanLine) ||
|
|
41
|
+
/^[โนโ]/u.test(cleanLine) ||
|
|
42
|
+
/\b(?:compiled successfully|build completed|done in)\b/i.test(cleanLine);
|
|
43
|
+
|
|
44
|
+
if (isNonErrorLine) {
|
|
45
|
+
inErrorBlock = false;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
30
49
|
const isNewHeader =
|
|
31
|
-
/^(?:\[?(?:error|fatal|fail|failed)\]?|error:|uncaught|fatal:)/i.test(
|
|
32
|
-
/^(?:TypeError|ReferenceError|SyntaxError|RangeError|EvalError|URIError|Error|UnhandledPromiseRejectionWarning|UnhandledPromiseRejection):/i.test(
|
|
33
|
-
/^npm ERR! (?:code|syscall|path|errno)/i.test(
|
|
34
|
-
/^\[vite\] Internal server error/i.test(
|
|
35
|
-
/\bTS\d{4,5}\b/i.test(
|
|
36
|
-
/\berror TS\d+/i.test(
|
|
37
|
-
/^(?:\[TypeScript\]|TS ERROR)/i.test(
|
|
50
|
+
/^(?:\[?(?:error|fatal|fail|failed)\]?|error:|uncaught|fatal:)/i.test(cleanLine) ||
|
|
51
|
+
/^(?:TypeError|ReferenceError|SyntaxError|RangeError|EvalError|URIError|Error|UnhandledPromiseRejectionWarning|UnhandledPromiseRejection):/i.test(cleanLine) ||
|
|
52
|
+
/^npm ERR! (?:code|syscall|path|errno)/i.test(cleanLine) ||
|
|
53
|
+
/^\[vite\] Internal server error/i.test(cleanLine) ||
|
|
54
|
+
/\bTS\d{4,5}\b/i.test(cleanLine) ||
|
|
55
|
+
/\berror TS\d+/i.test(cleanLine) ||
|
|
56
|
+
/^(?:\[TypeScript\]|TS ERROR)/i.test(cleanLine);
|
|
38
57
|
|
|
39
58
|
if (isNewHeader) {
|
|
40
59
|
inErrorBlock = false;
|
|
@@ -43,9 +62,9 @@ function countErrorBlocks(text, isStderr = false) {
|
|
|
43
62
|
const isErrorLine =
|
|
44
63
|
isStderr ||
|
|
45
64
|
isNewHeader ||
|
|
46
|
-
/\b(error|failed|fatal|exception)\b/i.test(
|
|
47
|
-
/^npm ERR!/i.test(
|
|
48
|
-
/\bTS\d{4,5}\b/i.test(
|
|
65
|
+
/\b(error|failed|fatal|exception)\b/i.test(cleanLine) ||
|
|
66
|
+
/^npm ERR!/i.test(cleanLine) ||
|
|
67
|
+
/\bTS\d{4,5}\b/i.test(cleanLine) ||
|
|
49
68
|
/^\s*at\s+[\w\d_$.<>]+\s+\(/i.test(line);
|
|
50
69
|
|
|
51
70
|
if (isErrorLine) {
|
|
@@ -217,6 +236,13 @@ export function runScript(launcher, script) {
|
|
|
217
236
|
child.on('exit', (code) => {
|
|
218
237
|
appendLog(key, code === 0 ? 'output' : 'error', `\nProcess exited with code ${code}.\n`);
|
|
219
238
|
const item = logs.get(key);
|
|
239
|
+
if (code !== null && code !== 0) {
|
|
240
|
+
addErrorRecord({
|
|
241
|
+
source: `Launcher: ${launcher.alias}`,
|
|
242
|
+
message: `Script "${script.name}" exited with code ${code}`,
|
|
243
|
+
details: item?.error || item?.output || `Command: ${script.command}`
|
|
244
|
+
});
|
|
245
|
+
}
|
|
220
246
|
if (item) item.errorCount = 0;
|
|
221
247
|
running.delete(key);
|
|
222
248
|
retireGroup(child.pid);
|
package/server.js
CHANGED
|
@@ -16,13 +16,48 @@ import prReviewRoutes from './server/routes/pr-review.js';
|
|
|
16
16
|
import jiraFiltersRoutes from './server/routes/jira-filters.js';
|
|
17
17
|
import todoRoutes from './server/routes/todos.js';
|
|
18
18
|
import staticPagesRoutes from './server/routes/static-pages.js';
|
|
19
|
+
import errorRoutes from './server/routes/errors.js';
|
|
20
|
+
import { addErrorRecord } from './server/repositories/errors.js';
|
|
19
21
|
import { startClipboardCapture } from './server/services/clipboard-history.js';
|
|
20
22
|
|
|
21
23
|
const port = Number(process.env.PORT || 3100);
|
|
22
24
|
const app = express();
|
|
23
25
|
app.use(express.json());
|
|
26
|
+
|
|
27
|
+
// Intercept error responses (HTTP status >= 400) from any API route
|
|
28
|
+
app.use((req, res, next) => {
|
|
29
|
+
const originalJson = res.json;
|
|
30
|
+
res.json = function (body) {
|
|
31
|
+
if (res.statusCode >= 400 && body && body.error && !req.path.startsWith('/api/errors')) {
|
|
32
|
+
const host = req.get('host') || req.headers.host || `localhost:${port}`;
|
|
33
|
+
const fullUrl = `${req.protocol}://${host}${req.originalUrl || req.url}`;
|
|
34
|
+
|
|
35
|
+
const detailsLines = [];
|
|
36
|
+
if (body.targetUrl) {
|
|
37
|
+
detailsLines.push(`Target URL: ${body.targetUrl}`);
|
|
38
|
+
}
|
|
39
|
+
detailsLines.push(`Endpoint: ${req.method} ${fullUrl}`);
|
|
40
|
+
detailsLines.push(`HTTP Status: ${res.statusCode}`);
|
|
41
|
+
if (typeof body.error === 'string' && body.error !== `HTTP ${res.statusCode} Error`) {
|
|
42
|
+
detailsLines.push(`Error: ${body.error}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
addErrorRecord({
|
|
46
|
+
source: `Backend API (${req.method} ${req.path})`,
|
|
47
|
+
message: typeof body.error === 'string' ? body.error : `HTTP ${res.statusCode} Error`,
|
|
48
|
+
details: detailsLines.join('\n')
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return originalJson.call(this, body);
|
|
52
|
+
};
|
|
53
|
+
next();
|
|
54
|
+
});
|
|
55
|
+
|
|
24
56
|
app.use(express.static(paths.ui));
|
|
25
57
|
app.use('/plugins', express.static(paths.plugins));
|
|
58
|
+
app.use('/pages', express.static(paths.pages));
|
|
59
|
+
app.use('/pages', express.static(paths.dataPages));
|
|
60
|
+
app.use('/data/pages', express.static(paths.dataPages));
|
|
26
61
|
app.use('/api/launchers', launcherRoutes);
|
|
27
62
|
app.use('/api/plugins', pluginRoutes);
|
|
28
63
|
app.use('/api/clipboard', clipboardRoutes);
|
|
@@ -33,6 +68,16 @@ app.use('/api/pr-review', prReviewRoutes);
|
|
|
33
68
|
app.use('/api/jira-filters', jiraFiltersRoutes);
|
|
34
69
|
app.use('/api/todos', todoRoutes);
|
|
35
70
|
app.use('/api/static-pages', staticPagesRoutes);
|
|
71
|
+
app.use('/api/errors', errorRoutes);
|
|
72
|
+
|
|
73
|
+
app.use((err, req, res, _next) => {
|
|
74
|
+
addErrorRecord({
|
|
75
|
+
source: `Backend API (${req.method} ${req.path})`,
|
|
76
|
+
message: err.message || 'Internal Server Error',
|
|
77
|
+
details: err.stack || String(err)
|
|
78
|
+
});
|
|
79
|
+
res.status(500).json({ error: err.message || 'Internal Server Error' });
|
|
80
|
+
});
|
|
36
81
|
|
|
37
82
|
await freePort(port);
|
|
38
83
|
const server = app.listen(port, () => { console.log(`Buddy Workbench: http://localhost:${port} (pid ${process.pid})`); shutdownTrace(`server started (pid=${process.pid})`); });
|
|
@@ -57,6 +102,14 @@ async function shutdown(exitCode = 0) {
|
|
|
57
102
|
process.once('SIGINT', () => { shutdownTrace('received SIGINT'); void shutdown(); });
|
|
58
103
|
process.once('SIGTERM', () => { shutdownTrace('received SIGTERM'); void shutdown(); });
|
|
59
104
|
process.once('SIGHUP', () => { shutdownTrace('received SIGHUP'); void shutdown(); });
|
|
60
|
-
process.once('uncaughtException', (error) => {
|
|
61
|
-
|
|
105
|
+
process.once('uncaughtException', (error) => {
|
|
106
|
+
addErrorRecord({ source: 'Server UncaughtException', message: error.message || 'Uncaught Server Exception', details: error.stack || String(error) });
|
|
107
|
+
console.error(error);
|
|
108
|
+
void shutdown(1);
|
|
109
|
+
});
|
|
110
|
+
process.once('unhandledRejection', (error) => {
|
|
111
|
+
addErrorRecord({ source: 'Server UnhandledRejection', message: error?.message || String(error || 'Unhandled Promise Rejection'), details: error?.stack || String(error) });
|
|
112
|
+
console.error(error);
|
|
113
|
+
void shutdown(1);
|
|
114
|
+
});
|
|
62
115
|
process.once('exit', () => stopAllScripts(true));
|