buddy-workbench 0.1.72 → 0.1.73

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.72",
3
+ "version": "0.1.73",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -59,5 +59,8 @@
59
59
  "@jsquash/jpeg": "1.6.0",
60
60
  "axios": "1.7.9",
61
61
  "express": "5.1.0"
62
+ },
63
+ "overrides": {
64
+ "content-type": "2.0.0"
62
65
  }
63
66
  }
@@ -3,8 +3,6 @@ import axios from 'axios';
3
3
  import { Router } from 'express';
4
4
  import { readSettings } from '../repositories/settings.js';
5
5
  import { listTodos } from '../repositories/todos.js';
6
- import { listBranchSyncApps } from '../repositories/branch-sync.js';
7
- import { getPresentations } from '../repositories/presentations.js';
8
6
  import { recordApiError } from '../lib/api-errors.js';
9
7
 
10
8
  const router = Router();
@@ -16,16 +14,6 @@ const httpClient = axios.create({
16
14
 
17
15
  const isSuccess = (response) => response.status >= 200 && response.status < 300;
18
16
  const apiMessage = (response, fallback) => response?.data?.errors?.[0]?.message || response?.data?.errorMessages?.[0] || response?.data?.message || response?.statusText || fallback;
19
- const getPresentationInsights = () => getPresentations().plans
20
- .slice()
21
- .sort((a, b) => Number(b.updatedAt || b.createdAt || 0) - Number(a.updatedAt || a.createdAt || 0))
22
- .map((plan) => ({
23
- id: plan.id,
24
- title: plan.title || plan.name || 'Untitled presentation',
25
- summary: `${Array.isArray(plan.sections) ? plan.sections.length : 0} sections`,
26
- updated: plan.updatedAt || plan.createdAt || null,
27
- url: '#/presentation-plan'
28
- }));
29
17
 
30
18
  function jiraHost(domain) {
31
19
  const clean = String(domain || '').replace(/^https?:\/\//i, '').replace(/\/+$/, '');
@@ -39,20 +27,7 @@ function bitbucketHost(domain) {
39
27
  return clean.includes('bitbucket') ? clean : `bitbucket.${clean}`;
40
28
  }
41
29
 
42
- function parseRepo(repo) {
43
- const raw = String(repo || '').replace(/\.git$/i, '');
44
- try {
45
- const url = new URL(raw.startsWith('ssh://') ? raw.replace(/^ssh:\/\//i, 'https://') : raw);
46
- const parts = url.pathname.split('/').filter(Boolean);
47
- if (parts[0] === 'scm' && parts.length >= 3) return { host: url.host, project: parts[1], slug: parts[2] };
48
- if ((parts[0] === 'projects' || parts[0] === 'users') && parts.length >= 4) return { host: url.host, project: parts[0] === 'users' ? `~${parts[1]}` : parts[1], slug: parts[3] };
49
- if (parts.length >= 2) return { host: url.host, project: parts.at(-2), slug: parts.at(-1) };
50
- } catch {}
51
- return null;
52
- }
53
-
54
30
  function jiraUrl(host, path) { return `https://${host}/rest/api/2${path}`; }
55
- function bitbucketUrl(host, path) { return `https://${host}/rest/api/latest${path}`; }
56
31
 
57
32
  async function requestSection(name, loader) {
58
33
  try {
@@ -95,10 +70,9 @@ async function loadJiraData(host, token) {
95
70
  const identity = { accountId: me.accountId || '', name: me.name || '', displayName: me.displayName || '' };
96
71
  const fields = 'summary,status,priority,duedate,updated,assignee,reporter';
97
72
 
98
- const [due, activity, blocked] = await Promise.all([
73
+ const [due, activity] = await Promise.all([
99
74
  searchJira(host, token, 'assignee = currentUser() AND due = startOfDay() AND resolution = Unresolved ORDER BY priority DESC', fields),
100
- searchJira(host, token, 'updated >= -14d AND resolution = Unresolved AND (assignee = currentUser() OR reporter = currentUser() OR watcher = currentUser()) ORDER BY updated DESC', fields, 30),
101
- searchJira(host, token, 'status = Blocked AND updated <= -3d AND resolution = Unresolved ORDER BY updated ASC', fields)
75
+ searchJira(host, token, 'updated >= -14d AND resolution = Unresolved AND (assignee = currentUser() OR reporter = currentUser() OR watcher = currentUser()) ORDER BY updated DESC', fields, 30)
102
76
  ]);
103
77
 
104
78
  const activityItems = [];
@@ -130,15 +104,14 @@ async function loadJiraData(host, token) {
130
104
  return {
131
105
  identity,
132
106
  dueToday: due.map((issue) => ({ ...mapJiraIssue(issue), url: issueUrl(host, issue.key) })),
133
- activity: activityItems.map((item) => ({ ...item, url: issueUrl(host, item.key) })),
134
- blocked: blocked.map((issue) => ({ ...mapJiraIssue(issue), url: issueUrl(host, issue.key) }))
107
+ activity: activityItems.map((item) => ({ ...item, url: issueUrl(host, item.key) }))
135
108
  };
136
109
  }
137
110
 
138
- async function loadPullRequests(host, token, role) {
139
- const url = `https://${host}/rest/api/latest/dashboard/pull-requests?role=${role}&state=OPEN&limit=100`;
111
+ async function loadAuthoredPullRequests(host, token) {
112
+ const url = `https://${host}/rest/api/latest/dashboard/pull-requests?role=author&state=OPEN&limit=100`;
140
113
  const response = await httpClient.get(url, { headers: { Accept: 'application/json', Authorization: `Bearer ${token}` } });
141
- if (!isSuccess(response)) throw new Error(`Bitbucket ${role} pull requests failed (${response.status}): ${apiMessage(response, 'Request failed')}`);
114
+ if (!isSuccess(response)) throw new Error(`Bitbucket author pull requests failed (${response.status}): ${apiMessage(response, 'Request failed')}`);
142
115
  return (response.data?.values || []).map((pr) => ({
143
116
  id: pr.id,
144
117
  title: pr.title || `Pull request #${pr.id}`,
@@ -149,65 +122,21 @@ async function loadPullRequests(host, token, role) {
149
122
  }));
150
123
  }
151
124
 
152
- async function loadFailedBuilds(host, token) {
153
- const settings = readSettings();
154
- const apps = listBranchSyncApps();
155
- const repos = [];
156
- for (const app of apps) {
157
- for (const branch of [...new Set([app.activeBranch, app.lastReleaseBranch, 'master'].filter(Boolean))]) {
158
- repos.push({ name: app.name, repo: app.repo, branch });
159
- }
160
- for (const sub of app.subApps || []) {
161
- for (const branch of [...new Set([sub.branch, app.activeBranch, 'master'].filter(Boolean))]) {
162
- repos.push({ name: sub.name || sub.directory, repo: sub.repo, branch });
163
- }
164
- }
165
- }
166
- const unique = [...new Map(repos.map((item) => [`${item.repo}|${item.branch}`, item])).values()];
167
- const failed = [];
168
- for (const item of unique.slice(0, 30)) {
169
- const parsed = parseRepo(item.repo);
170
- if (!parsed || (parsed.host && parsed.host !== host && !parsed.host.includes('bitbucket'))) continue;
171
- const commitUrl = bitbucketUrl(parsed.host || host, `/projects/${encodeURIComponent(parsed.project)}/repos/${encodeURIComponent(parsed.slug)}/commits?until=${encodeURIComponent(item.branch)}&limit=1`);
172
- const commitResponse = await httpClient.get(commitUrl, { headers: { Accept: 'application/json', Authorization: `Bearer ${token}` } });
173
- if (!isSuccess(commitResponse)) {
174
- recordApiError({ source: 'Overview (Bitbucket Builds)', method: 'GET', url: commitUrl, status: commitResponse.status, message: apiMessage(commitResponse, 'Failed to fetch branch commit.') });
175
- continue;
176
- }
177
- const hash = commitResponse.data?.values?.[0]?.id;
178
- if (!hash) continue;
179
- const buildUrl = `https://${parsed.host || host}/rest/api/latest/projects/${encodeURIComponent(parsed.project)}/repos/${encodeURIComponent(parsed.slug)}/commits/${encodeURIComponent(hash)}/builds`;
180
- const buildResponse = await httpClient.get(buildUrl, { headers: { Accept: 'application/json', Authorization: `Bearer ${token}` } });
181
- if (!isSuccess(buildResponse)) {
182
- if (buildResponse.status !== 404) recordApiError({ source: 'Overview (Bitbucket Builds)', method: 'GET', url: buildUrl, status: buildResponse.status, message: apiMessage(buildResponse, 'Failed to fetch build status.') });
183
- continue;
184
- }
185
- for (const build of buildResponse.data?.values || []) {
186
- if (String(build.state || '').toUpperCase() === 'FAILED') {
187
- failed.push({ name: item.name, branch: item.branch, repository: item.repo, build: build.name || build.key || 'Build', date: build.dateAdded || null, url: item.repo });
188
- }
189
- }
190
- }
191
- return failed;
192
- }
193
-
194
125
  router.get('/', async (_req, res) => {
195
126
  const settings = readSettings();
196
- const missingTokens = ['jira', 'bitbucket', 'confluence'].filter((service) => !settings[`${service}AccessToken`]);
127
+ const missingTokens = ['jira', 'bitbucket'].filter((service) => !settings[`${service}AccessToken`]);
197
128
  if (missingTokens.length > 0) {
198
- return res.json({ configured: false, missingTokens, fetchedAt: new Date().toISOString(), categories: { presentations: getPresentationInsights() } });
129
+ return res.json({ configured: false, missingTokens, fetchedAt: new Date().toISOString(), categories: {} });
199
130
  }
200
131
  const jira = jiraHost(settings.domain);
201
132
  const bitbucket = bitbucketHost(settings.domain);
202
- if (!jira || !bitbucket) return res.json({ configured: false, missingTokens: ['domain'], fetchedAt: new Date().toISOString(), categories: { presentations: getPresentationInsights() } });
133
+ if (!jira || !bitbucket) return res.json({ configured: false, missingTokens: ['domain'], fetchedAt: new Date().toISOString(), categories: {} });
203
134
 
204
135
  const sections = await Promise.all([
205
136
  requestSection('Jira', () => loadJiraData(jira, settings.jiraAccessToken)),
206
- requestSection('Bitbucket Review', () => loadPullRequests(bitbucket, settings.bitbucketAccessToken, 'reviewer')),
207
- requestSection('Bitbucket Author', () => loadPullRequests(bitbucket, settings.bitbucketAccessToken, 'author')),
208
- requestSection('Bitbucket Builds', () => loadFailedBuilds(bitbucket, settings.bitbucketAccessToken))
137
+ requestSection('Bitbucket Author', () => loadAuthoredPullRequests(bitbucket, settings.bitbucketAccessToken))
209
138
  ]);
210
- const [jiraData, reviewPrs, authoredPrs, failedBuilds] = sections;
139
+ const [jiraData, authoredPrs] = sections;
211
140
  const todos = listTodos().filter((todo) => !todo.completed && !todo.archived).map((todo) => ({ ...todo, url: '#/todo-list' }));
212
141
  const errors = sections.filter((section) => section.error).map((section) => section.error);
213
142
  res.json({
@@ -215,14 +144,10 @@ router.get('/', async (_req, res) => {
215
144
  missingTokens: [],
216
145
  fetchedAt: new Date().toISOString(),
217
146
  categories: {
147
+ activity: jiraData.items?.activity || [],
218
148
  jiraDueToday: jiraData.items?.dueToday || [],
219
149
  assignedTodos: todos,
220
- reviewPrs: reviewPrs.items || [],
221
- authoredPrs: authoredPrs.items || [],
222
- failedBuilds: failedBuilds.items || [],
223
- activity: jiraData.items?.activity || [],
224
- blockedIssues: jiraData.items?.blocked || [],
225
- presentations: getPresentationInsights()
150
+ authoredPrs: authoredPrs.items || []
226
151
  },
227
152
  errors
228
153
  });