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.
|
|
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
|
|
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
|
|
139
|
-
const url = `https://${host}/rest/api/latest/dashboard/pull-requests?role
|
|
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
|
|
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'
|
|
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: {
|
|
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: {
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
});
|