buddy-workbench 0.1.7 → 0.1.9
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
CHANGED
|
@@ -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);
|
|
@@ -375,4 +375,59 @@ router.get('/review-prs', async (req, res) => {
|
|
|
375
375
|
}
|
|
376
376
|
});
|
|
377
377
|
|
|
378
|
+
router.post('/comment', async (req, res) => {
|
|
379
|
+
const { projectKey, repositorySlug, pullRequestId, filePath, line, commentText } = req.body || {};
|
|
380
|
+
if (!projectKey || !repositorySlug || !pullRequestId || !commentText) {
|
|
381
|
+
return res.status(400).json({ error: 'Missing required parameters.' });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const host = getBitbucketHost();
|
|
385
|
+
const settings = readSettings();
|
|
386
|
+
const token = settings.bitbucketAccessToken;
|
|
387
|
+
|
|
388
|
+
if (!host || !token) {
|
|
389
|
+
return res.status(400).json({ error: 'Bitbucket Access Token or Host not configured.' });
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const url = `https://${host}/rest/api/1.0/projects/${projectKey}/repos/${repositorySlug}/pull-requests/${pullRequestId}/comments`;
|
|
393
|
+
const headers = {
|
|
394
|
+
'Content-Type': 'application/json',
|
|
395
|
+
'Accept': 'application/json',
|
|
396
|
+
'Authorization': `Bearer ${token}`
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const payload = {
|
|
400
|
+
text: commentText
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
if (filePath && line) {
|
|
404
|
+
payload.anchor = {
|
|
405
|
+
line: parseInt(line, 10),
|
|
406
|
+
lineType: 'ADDED',
|
|
407
|
+
fileType: 'TO',
|
|
408
|
+
path: filePath
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
const response = await fetch(url, {
|
|
414
|
+
method: 'POST',
|
|
415
|
+
headers,
|
|
416
|
+
body: JSON.stringify(payload)
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
if (!response.ok) {
|
|
420
|
+
const errJson = await response.json().catch(() => ({}));
|
|
421
|
+
return res.status(response.status).json({
|
|
422
|
+
error: errJson.errors?.[0]?.message || errJson.message || `Bitbucket API error: ${response.statusText}`
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const data = await response.json();
|
|
427
|
+
res.json({ success: true, data });
|
|
428
|
+
} catch (error) {
|
|
429
|
+
res.status(500).json({ error: error.message });
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
378
433
|
export default router;
|
|
@@ -14,13 +14,69 @@ const savedPortHistory = new Map(listPortHistory().map((record) => [record.port,
|
|
|
14
14
|
const supervisorPath = fileURLToPath(new URL('./script-supervisor.js', import.meta.url));
|
|
15
15
|
const execFileAsync = promisify(execFile);
|
|
16
16
|
const keyFor = (launcherId, scriptId) => `${launcherId}:${scriptId}`;
|
|
17
|
+
function countErrorBlocks(text, isStderr = false) {
|
|
18
|
+
if (!text) return 0;
|
|
19
|
+
const lines = text.split(/\r?\n/);
|
|
20
|
+
let count = 0;
|
|
21
|
+
let inErrorBlock = false;
|
|
22
|
+
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
const trimmed = line.trim();
|
|
25
|
+
if (!trimmed) {
|
|
26
|
+
inErrorBlock = false;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const isNewHeader =
|
|
31
|
+
/^(?:\[?(?:error|fatal|fail|failed)\]?|error:|uncaught|fatal:)/i.test(trimmed) ||
|
|
32
|
+
/^(?:TypeError|ReferenceError|SyntaxError|RangeError|EvalError|URIError|Error|UnhandledPromiseRejectionWarning|UnhandledPromiseRejection):/i.test(trimmed) ||
|
|
33
|
+
/^npm ERR! (?:code|syscall|path|errno)/i.test(trimmed) ||
|
|
34
|
+
/^\[vite\] Internal server error/i.test(trimmed) ||
|
|
35
|
+
/\bTS\d{4,5}\b/i.test(trimmed) ||
|
|
36
|
+
/\berror TS\d+/i.test(trimmed) ||
|
|
37
|
+
/^(?:\[TypeScript\]|TS ERROR)/i.test(trimmed);
|
|
38
|
+
|
|
39
|
+
if (isNewHeader) {
|
|
40
|
+
inErrorBlock = false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const isErrorLine =
|
|
44
|
+
isStderr ||
|
|
45
|
+
isNewHeader ||
|
|
46
|
+
/\b(error|failed|fatal|exception)\b/i.test(trimmed) ||
|
|
47
|
+
/^npm ERR!/i.test(trimmed) ||
|
|
48
|
+
/\bTS\d{4,5}\b/i.test(trimmed) ||
|
|
49
|
+
/^\s*at\s+[\w\d_$.<>]+\s+\(/i.test(line);
|
|
50
|
+
|
|
51
|
+
if (isErrorLine) {
|
|
52
|
+
if (!inErrorBlock) {
|
|
53
|
+
count++;
|
|
54
|
+
inErrorBlock = true;
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
inErrorBlock = false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return count;
|
|
62
|
+
}
|
|
63
|
+
|
|
17
64
|
const appendLog = (key, type, chunk) => {
|
|
18
|
-
const current = logs.get(key) || { output: '', error: '' };
|
|
19
|
-
|
|
65
|
+
const current = logs.get(key) || { output: '', error: '', errorCount: 0 };
|
|
66
|
+
const str = String(chunk || '');
|
|
67
|
+
current[type] = `${current[type]}${str}`.split(/\r?\n/).slice(-1000).join('\n');
|
|
68
|
+
current.errorCount = countErrorBlocks(current.error, true) + countErrorBlocks(current.output, false);
|
|
20
69
|
logs.set(key, current);
|
|
21
70
|
};
|
|
22
71
|
|
|
23
72
|
export function runningScripts() { return [...running.keys()]; }
|
|
73
|
+
export function runningErrorCounts() {
|
|
74
|
+
const counts = {};
|
|
75
|
+
for (const [key, data] of logs.entries()) {
|
|
76
|
+
if (running.has(key) && data?.errorCount) counts[key] = data.errorCount;
|
|
77
|
+
}
|
|
78
|
+
return counts;
|
|
79
|
+
}
|
|
24
80
|
export function scriptLogs(launcherId, scriptId) { return logs.get(keyFor(launcherId, scriptId))?.output || ''; }
|
|
25
81
|
export function scriptErrorLogs(launcherId, scriptId) { return logs.get(keyFor(launcherId, scriptId))?.error || ''; }
|
|
26
82
|
|
|
@@ -149,7 +205,7 @@ export function runScript(launcher, script) {
|
|
|
149
205
|
const key = keyFor(launcher.id, script.id);
|
|
150
206
|
if (running.has(key)) throw new Error('This script is already running.');
|
|
151
207
|
if (!existsSync(launcher.folder)) throw new Error('The configured project folder does not exist.');
|
|
152
|
-
logs.set(key, { output: `$ ${script.command}\n`, error: '' });
|
|
208
|
+
logs.set(key, { output: `$ ${script.command}\n`, error: '', errorCount: 0 });
|
|
153
209
|
const child = spawn(process.execPath, [supervisorPath], { cwd: launcher.folder, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, ...(launcher.executor === 'pnpm' || /^\s*pnpm\b/.test(script.command) ? { CI: 'true' } : {}), BUDDY_PARENT_PID: String(process.pid), BUDDY_SCRIPT_COMMAND: script.command } });
|
|
154
210
|
child.stdout.on('data', (chunk) => appendLog(key, 'output', chunk.toString()));
|
|
155
211
|
child.stderr.on('data', (chunk) => appendLog(key, 'error', chunk.toString()));
|
|
@@ -158,7 +214,13 @@ export function runScript(launcher, script) {
|
|
|
158
214
|
launchHistory.push({ key, groupPid: child.pid, launcher: launcher.alias, script: script.name, startedAt: new Date().toISOString(), ports: new Map() });
|
|
159
215
|
if (launchHistory.length > 200) launchHistory.splice(0, launchHistory.length - 200);
|
|
160
216
|
setTimeout(() => { void observeLauncherPorts().catch(() => {}); }, 1000).unref();
|
|
161
|
-
child.on('exit', (code) => {
|
|
217
|
+
child.on('exit', (code) => {
|
|
218
|
+
appendLog(key, code === 0 ? 'output' : 'error', `\nProcess exited with code ${code}.\n`);
|
|
219
|
+
const item = logs.get(key);
|
|
220
|
+
if (item) item.errorCount = 0;
|
|
221
|
+
running.delete(key);
|
|
222
|
+
retireGroup(child.pid);
|
|
223
|
+
});
|
|
162
224
|
}
|
|
163
225
|
|
|
164
226
|
export function stopScript(launcherId, scriptId) {
|
|
@@ -166,6 +228,8 @@ export function stopScript(launcherId, scriptId) {
|
|
|
166
228
|
const child = running.get(key);
|
|
167
229
|
if (!child) throw new Error('This script is not running.');
|
|
168
230
|
appendLog(key, 'output', '\nStop requested.\n');
|
|
231
|
+
const item = logs.get(key);
|
|
232
|
+
if (item) item.errorCount = 0;
|
|
169
233
|
stoppingGroups.add(child.pid);
|
|
170
234
|
terminateGroup(child.pid, 'SIGTERM');
|
|
171
235
|
setTimeout(() => { terminateGroup(child.pid, 'SIGKILL'); stoppingGroups.delete(child.pid); processGroups.delete(child.pid); }, 1500).unref();
|