buddy-workbench 0.1.42 → 0.1.44

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.42",
3
+ "version": "0.1.44",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -91,11 +91,15 @@ router.post('/browse', async (req, res) => {
91
91
  name: e.name,
92
92
  fullPath: path.join(dirPath, e.name)
93
93
  }));
94
+ const files = entries
95
+ .filter(e => e.isFile() && !e.name.startsWith('.'))
96
+ .map(e => ({ name: e.name, fullPath: path.join(dirPath, e.name) }));
94
97
 
95
98
  res.json({
96
99
  currentPath: dirPath,
97
100
  parentPath: path.dirname(dirPath),
98
- subdirectories: subdirs
101
+ subdirectories: subdirs,
102
+ files
99
103
  });
100
104
  } catch (err) {
101
105
  res.status(500).json({ error: err.message });
@@ -270,6 +270,7 @@ router.post('/issues/clone', async (req, res) => {
270
270
  // Fetch original issue details to preserve issue type, project, description, priority
271
271
  let origIssueType = null;
272
272
  let origProjectKey = null;
273
+ let origSummary = null;
273
274
  let origDescription = null;
274
275
  let origPriority = null;
275
276
 
@@ -289,6 +290,9 @@ router.post('/issues/clone', async (req, res) => {
289
290
  if (fields.project?.key) {
290
291
  origProjectKey = fields.project.key;
291
292
  }
293
+ if (fields.summary) {
294
+ origSummary = String(fields.summary).trim();
295
+ }
292
296
  if (fields.description) {
293
297
  origDescription = fields.description;
294
298
  }
@@ -308,10 +312,12 @@ router.post('/issues/clone', async (req, res) => {
308
312
  // Determine issue type fallback: fetched original issueType -> passed issueType -> Task
309
313
  const targetIssueType = origIssueType || (issueType ? { name: issueType } : { name: 'Task' });
310
314
  const projectKey = origProjectKey || (issueKey ? issueKey.split('-')[0] : '');
315
+ const requestedSummary = summary.trim();
316
+ const cloneSummary = origSummary || requestedSummary;
311
317
  const url = `https://${jiraHost}/rest/api/2/issue`;
312
318
  const payload = {
313
319
  fields: {
314
- summary: summary.trim(),
320
+ summary: cloneSummary,
315
321
  ...(projectKey ? { project: { key: projectKey } } : {}),
316
322
  issuetype: targetIssueType,
317
323
  ...(assigneeField ? { assignee: assigneeField } : {}),
@@ -325,7 +331,7 @@ router.post('/issues/clone', async (req, res) => {
325
331
  if (response.status !== 201 && response.status !== 200 && (origPriority || origDescription)) {
326
332
  const fallbackPayload = {
327
333
  fields: {
328
- summary: summary.trim(),
334
+ summary: cloneSummary,
329
335
  ...(projectKey ? { project: { key: projectKey } } : {}),
330
336
  issuetype: targetIssueType,
331
337
  ...(assigneeField ? { assignee: assigneeField } : {})
@@ -355,6 +361,20 @@ router.post('/issues/clone', async (req, res) => {
355
361
  }
356
362
  }
357
363
 
364
+ // Clone with the original summary first, then apply the requested summary.
365
+ // This keeps both Clone Ticket and + Jira on the same clone-then-edit path.
366
+ if (createdKey && requestedSummary !== cloneSummary) {
367
+ const updateUrl = `https://${jiraHost}/rest/api/2/issue/${encodeURIComponent(createdKey)}`;
368
+ const updateResponse = await httpClient.put(updateUrl, { fields: { summary: requestedSummary } }, { headers });
369
+ if (updateResponse.status < 200 || updateResponse.status >= 300) {
370
+ const errMsg = updateResponse.data?.errorMessages?.[0] ||
371
+ (updateResponse.data?.errors ? Object.values(updateResponse.data.errors).join(', ') : null) ||
372
+ updateResponse.data?.message ||
373
+ `Jira summary update returned status ${updateResponse.status}`;
374
+ return res.status(updateResponse.status).json({ error: errMsg, createdKey });
375
+ }
376
+ }
377
+
358
378
  const issueUrl = `https://${jiraHost}/browse/${createdKey}`;
359
379
  if (trackRecent) {
360
380
  const recentIssue = {
@@ -72,9 +72,9 @@ const analyzeDiff = (filePath, hunks) => {
72
72
  let segmentText = '';
73
73
  const charToLineMap = [];
74
74
 
75
- for (const line of lines) {
76
- const text = line.text || '';
77
- const lineNum = line.destinationLine;
75
+ for (const lineObj of lines) {
76
+ const text = lineObj.line ?? lineObj.text ?? '';
77
+ const lineNum = lineObj.destination ?? lineObj.destinationLine ?? 1;
78
78
  const startIdx = segmentText.length;
79
79
  segmentText += text + '\n';
80
80
  const endIdx = segmentText.length;
@@ -87,7 +87,9 @@ const analyzeDiff = (filePath, hunks) => {
87
87
  return { lineNum: item.lineNum, text: item.text };
88
88
  }
89
89
  }
90
- return { lineNum: lines[0]?.destinationLine || 1, text: lines[0]?.text || '' };
90
+ const firstText = lines[0]?.line ?? lines[0]?.text ?? '';
91
+ const firstLine = lines[0]?.destination ?? lines[0]?.destinationLine ?? 1;
92
+ return { lineNum: firstLine, text: firstText };
91
93
  };
92
94
 
93
95
  const addIssue = (severity, rule, message, charIndex, customCode) => {
@@ -104,10 +106,10 @@ const analyzeDiff = (filePath, hunks) => {
104
106
  };
105
107
 
106
108
  // Rule 1: Object property extraction requires lodash.get
107
- const propAccessRegex = /\b([a-zA-Z_$][\w$]*)\s*\.\s*([a-zA-Z_$][\w$]*)\b(?!\s*\()/g;
109
+ const propAccessRegex = /\b([a-zA-Z_$][\w$]*)\s*(\?\.|\.)\s*([a-zA-Z_$][\w$]*)\b(?!\s*\()/g;
108
110
  for (const match of segmentText.matchAll(propAccessRegex)) {
109
111
  const objName = match[1];
110
- const propName = match[2];
112
+ const propName = match[3];
111
113
  const matchIdx = match.index;
112
114
 
113
115
  if (objName === 'row' && propName === 'original') continue;
@@ -116,7 +118,8 @@ const analyzeDiff = (filePath, hunks) => {
116
118
  const globalNamespaces = [
117
119
  'Math', 'Object', 'Array', 'JSON', 'Promise', 'Reflect', 'Symbol', 'Date',
118
120
  'Number', 'String', 'Boolean', 'React', 'process', 'e', 'evt', 'event',
119
- 'console', 'window', 'document', 'history', 'location', 'styles', 'style'
121
+ 'console', 'window', 'document', 'history', 'location', 'styles', 'style',
122
+ 'req', 'res'
120
123
  ];
121
124
  if (globalNamespaces.includes(objName)) continue;
122
125
 
@@ -366,7 +369,7 @@ router.post('/check', async (req, res) => {
366
369
 
367
370
  try {
368
371
  // 1. Fetch Pull Request details to show basic info
369
- const prUrl = `https://${host}/rest/api/1.0/projects/${projectKey}/repos/${repositorySlug}/pull-requests/${pullRequestId}`;
372
+ const prUrl = `https://${host}/rest/api/latest/projects/${projectKey}/repos/${repositorySlug}/pull-requests/${pullRequestId}`;
370
373
  const prRes = await httpClient.get(prUrl, { headers });
371
374
  if (prRes.status < 200 || prRes.status >= 300) {
372
375
  if (prRes.status === 401) {
@@ -377,7 +380,7 @@ router.post('/check', async (req, res) => {
377
380
  const prInfo = prRes.data;
378
381
 
379
382
  // 2. Fetch changes
380
- const changesUrl = `https://${host}/rest/api/1.0/projects/${projectKey}/repos/${repositorySlug}/pull-requests/${pullRequestId}/changes?limit=1000`;
383
+ const changesUrl = `https://${host}/rest/api/latest/projects/${projectKey}/repos/${repositorySlug}/pull-requests/${pullRequestId}/changes?limit=1000`;
381
384
  const changesRes = await httpClient.get(changesUrl, { headers });
382
385
  if (changesRes.status < 200 || changesRes.status >= 300) {
383
386
  return res.status(changesRes.status).json({ error: `Failed to fetch PR changes: ${changesRes.statusText || changesRes.status}`, targetUrl: changesUrl });
@@ -404,14 +407,24 @@ router.post('/check', async (req, res) => {
404
407
  let warningCount = 0;
405
408
  let infoCount = 0;
406
409
 
410
+ const untilCommit = prInfo.fromRef?.latestCommit || prInfo.fromRef?.id || 'HEAD';
411
+ const sinceCommit = prInfo.toRef?.latestCommit || prInfo.toRef?.id;
412
+
407
413
  for (const change of targetChanges) {
408
414
  const filePath = change.path.toString;
409
- const diffUrl = `https://${host}/rest/api/1.0/projects/${projectKey}/repos/${repositorySlug}/pull-requests/${pullRequestId}/diff/${encodeURIComponent(filePath)}?context=0`;
415
+ let diffUrl = `https://${host}/rest/api/latest/projects/${projectKey}/repos/${repositorySlug}/commits/${encodeURIComponent(untilCommit)}/diff/${filePath}?contextLines=10000&whitespace=ignore-all&withComments=false`;
416
+ if (sinceCommit) diffUrl += `&since=${encodeURIComponent(sinceCommit)}`;
417
+
410
418
  const diffRes = await httpClient.get(diffUrl, { headers });
411
419
  if (diffRes.status < 200 || diffRes.status >= 300) continue; // skip file if diff cannot be retrieved
412
420
 
413
421
  const diffData = diffRes.data;
414
- const fileIssues = analyzeDiff(filePath, diffData.hunks);
422
+ let hunks = diffData.hunks;
423
+ if (!hunks && Array.isArray(diffData.diffs)) {
424
+ hunks = diffData.diffs.flatMap(d => d.hunks || []);
425
+ }
426
+
427
+ const fileIssues = analyzeDiff(filePath, hunks || []);
415
428
 
416
429
  if (fileIssues.length > 0) {
417
430
  reviewFiles.push({
@@ -486,7 +499,7 @@ router.get('/my-prs', async (req, res) => {
486
499
  const headers = { 'Accept': 'application/json' };
487
500
  if (token) headers['Authorization'] = `Bearer ${token}`;
488
501
 
489
- const url = `https://${host}/rest/api/1.0/dashboard/pull-requests?role=author&state=OPEN&limit=100`;
502
+ const url = `https://${host}/rest/api/latest/dashboard/pull-requests?role=author&state=OPEN&limit=100`;
490
503
  try {
491
504
  const response = await httpClient.get(url, { headers });
492
505
  if (response.status < 200 || response.status >= 300) {
@@ -520,7 +533,7 @@ router.get('/review-prs', async (req, res) => {
520
533
  const headers = { 'Accept': 'application/json' };
521
534
  if (token) headers['Authorization'] = `Bearer ${token}`;
522
535
 
523
- const url = `https://${host}/rest/api/1.0/dashboard/pull-requests?role=reviewer&state=OPEN&limit=100`;
536
+ const url = `https://${host}/rest/api/latest/dashboard/pull-requests?role=reviewer&state=OPEN&limit=100`;
524
537
  try {
525
538
  const response = await httpClient.get(url, { headers });
526
539
  if (response.status < 200 || response.status >= 300) {
@@ -554,7 +567,7 @@ router.post('/comment', async (req, res) => {
554
567
  return res.status(400).json({ error: 'Bitbucket Access Token or Host not configured.' });
555
568
  }
556
569
 
557
- const url = `https://${host}/rest/api/1.0/projects/${projectKey}/repos/${repositorySlug}/pull-requests/${pullRequestId}/comments`;
570
+ const url = `https://${host}/rest/api/latest/projects/${projectKey}/repos/${repositorySlug}/pull-requests/${pullRequestId}/comments`;
558
571
  const headers = {
559
572
  'Content-Type': 'application/json',
560
573
  'Accept': 'application/json',
@@ -4,7 +4,7 @@ import { paths } from '../config.js';
4
4
  import { saveAccessToken, saveClipboardDeduplicateMinutes, saveClipboardEnabled, saveClipboardImageEnabled, saveDefaultBrowser, saveDefaultEditor, saveDomain, saveJiraIssuePrefix, saveTheme, settingsStatus } from '../repositories/settings.js';
5
5
  import { getNpmPackageVersions, installNvmVersion, manageGlobalNpmPackage, readDevConfigurations, readNvmConfiguration, saveDevConfigurations, searchNpmPackages } from '../services/dev-configurations.js';
6
6
  import { openInSystemBrowser, openInSystemEditor, openInSystemFolder } from '../services/browser.js';
7
- import { selectDirectory } from '../services/dialog.js';
7
+ import { selectDirectory, selectFile } from '../services/dialog.js';
8
8
 
9
9
  const router = Router();
10
10
 
@@ -93,6 +93,14 @@ router.post('/select-directory', async (_req, res) => {
93
93
  res.status(500).json({ error: error.message });
94
94
  }
95
95
  });
96
+ router.post('/select-file', async (_req, res) => {
97
+ try {
98
+ const result = await selectFile();
99
+ res.json(result);
100
+ } catch (error) {
101
+ res.status(500).json({ error: error.message });
102
+ }
103
+ });
96
104
  router.put('/clipboard-enabled', (req, res) => {
97
105
  const { enabled } = req.body || {};
98
106
  if (typeof enabled !== 'boolean') return res.status(400).json({ error: 'Enabled must be a boolean.' });
@@ -1,4 +1,4 @@
1
- import { execFile } from 'node:child_process';
1
+ import { execFile, spawn } from 'node:child_process';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { promisify } from 'node:util';
@@ -157,7 +157,19 @@ router.post('/self-update', async (_req, res) => {
157
157
  );
158
158
  cachedResult = null;
159
159
  cachedAt = 0;
160
- return res.json({ success: true, message: stdout.trim() || 'DevBuddy was updated successfully.' });
160
+ const message = stdout.trim() || 'DevBuddy was updated successfully.';
161
+ res.json({ success: true, message: `${message} Restarting DevBuddy…` });
162
+
163
+ // Let the update request finish before replacing the running server process.
164
+ setTimeout(() => {
165
+ const restartProcess = spawn(
166
+ process.execPath,
167
+ [join(root, 'bin', 'devbuddy.js'), 'restart'],
168
+ { cwd: root, detached: true, stdio: 'ignore', windowsHide: true }
169
+ );
170
+ restartProcess.unref();
171
+ }, 1000);
172
+ return undefined;
161
173
  } catch (error) {
162
174
  const details = error?.stderr?.trim() || error?.stdout?.trim() || error?.message;
163
175
  return res.status(500).json({ error: details || 'DevBuddy could not be updated.' });
@@ -41,3 +41,42 @@ export async function selectDirectory() {
41
41
  }
42
42
  }
43
43
  }
44
+
45
+ export async function selectFile() {
46
+ if (process.platform === 'darwin') {
47
+ try {
48
+ const { stdout } = await execFileAsync('osascript', [
49
+ '-e',
50
+ 'POSIX path of (choose file with prompt "Select SSH Key")'
51
+ ]);
52
+ const filePath = stdout.trim();
53
+ return filePath ? { path: filePath, canceled: false } : { path: null, canceled: true };
54
+ } catch {
55
+ return { path: null, canceled: true };
56
+ }
57
+ } else if (process.platform === 'win32') {
58
+ try {
59
+ const psScript = `
60
+ Add-Type -AssemblyName System.Windows.Forms
61
+ $dialog = New-Object System.Windows.Forms.OpenFileDialog
62
+ $dialog.Title = "Select SSH Key"
63
+ if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
64
+ Write-Output $dialog.FileName
65
+ }
66
+ `;
67
+ const { stdout } = await execFileAsync('powershell', ['-NoProfile', '-Command', psScript]);
68
+ const filePath = stdout.trim();
69
+ return filePath ? { path: filePath, canceled: false } : { path: null, canceled: true };
70
+ } catch {
71
+ return { path: null, canceled: true };
72
+ }
73
+ } else {
74
+ try {
75
+ const { stdout } = await execFileAsync('zenity', ['--file-selection', '--title=Select SSH Key']);
76
+ const filePath = stdout.trim();
77
+ return filePath ? { path: filePath, canceled: false } : { path: null, canceled: true };
78
+ } catch {
79
+ return { path: null, canceled: true };
80
+ }
81
+ }
82
+ }