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
  {
2
2
  "name": "buddy-workbench",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- current[type] = `${current[type]}${chunk}`.split(/\r?\n/).slice(-1000).join('\n');
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) => { appendLog(key, code === 0 ? 'output' : 'error', `\nProcess exited with code ${code}.\n`); running.delete(key); retireGroup(child.pid); });
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();