buddy-workbench 0.1.48 → 0.1.49
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 +1 -1
- package/server/routes/package-upgrade.js +56 -1
- package/server/services/clipboard-history.js +24 -4
- package/ui/dist/assets/index-DiVrDFQy.js +547 -0
- package/ui/dist/assets/index-m-P1BaNT.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/sw.js +2 -2
- package/ui/dist/assets/index-JbMPK4Jp.js +0 -547
- package/ui/dist/assets/index-K9g69_HI.css +0 -1
package/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import https from 'node:https';
|
|
1
2
|
import axios from 'axios';
|
|
2
3
|
import { Router } from 'express';
|
|
3
4
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
@@ -11,6 +12,11 @@ import { recordApiError } from '../lib/api-errors.js';
|
|
|
11
12
|
const router = Router();
|
|
12
13
|
const exec = promisify(execFile);
|
|
13
14
|
const running = new Set();
|
|
15
|
+
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
16
|
+
const httpClient = axios.create({
|
|
17
|
+
httpsAgent,
|
|
18
|
+
validateStatus: () => true
|
|
19
|
+
});
|
|
14
20
|
|
|
15
21
|
function repoName(url) { return basename(String(url || '').replace(/[\\/]$/, '').replace(/\.git$/i, '').replace(/[:/]+$/, '')) || 'repo'; }
|
|
16
22
|
function repoPath(data, repo) { return join(data.workspaceDir, repo.directory || repoName(repo.url)); }
|
|
@@ -24,6 +30,35 @@ function parseBitbucket(url) {
|
|
|
24
30
|
if (parts[0] === 'projects' || parts[0] === 'users') return { protocol, host: u.host, project: parts[1], slug: parts[3] };
|
|
25
31
|
return parts.length >= 2 ? { protocol, host: u.host, project: parts.at(-2), slug: parts.at(-1) } : null;
|
|
26
32
|
}
|
|
33
|
+
function bitbucketHeaders() {
|
|
34
|
+
const settings = readSettings();
|
|
35
|
+
const headers = { Accept: 'application/json' };
|
|
36
|
+
if (settings.bitbucketAccessToken) headers.Authorization = `Bearer ${settings.bitbucketAccessToken}`;
|
|
37
|
+
return headers;
|
|
38
|
+
}
|
|
39
|
+
function branchTimestamp(commit) {
|
|
40
|
+
const value = commit?.committerTimestamp || commit?.authorTimestamp || commit?.committer?.timestamp || commit?.author?.timestamp || commit?.date;
|
|
41
|
+
const timestamp = typeof value === 'number' ? value : Date.parse(value || '');
|
|
42
|
+
return Number.isNaN(timestamp) ? 0 : timestamp;
|
|
43
|
+
}
|
|
44
|
+
async function fetchRemoteBranches(repo) {
|
|
45
|
+
const parsed = parseBitbucket(repo.url);
|
|
46
|
+
if (!parsed) throw new Error(`Could not parse Bitbucket repository URL for ${repo.name || repoName(repo.url)}.`);
|
|
47
|
+
const base = `${parsed.protocol}://${parsed.host}/rest/api/latest/projects/${encodeURIComponent(parsed.project)}/repos/${encodeURIComponent(parsed.slug)}`;
|
|
48
|
+
const response = await httpClient.get(`${base}/branches?limit=200&orderBy=MODIFICATION`, { headers: bitbucketHeaders() });
|
|
49
|
+
if (response.status < 200 || response.status >= 300) throw new Error(response.data?.errors?.[0]?.message || `Bitbucket branches failed (${response.status})`);
|
|
50
|
+
const values = Array.isArray(response.data?.values) ? response.data.values : [];
|
|
51
|
+
return { base, branches: values.map((branch, index) => ({ name: branch.displayId || branch.id?.replace(/^refs\/heads\//, ''), latestCommit: branch.latestCommit, rank: index })).filter((branch) => branch.name) };
|
|
52
|
+
}
|
|
53
|
+
async function fetchBranchUpdatedAt(base, branch) {
|
|
54
|
+
if (!branch.latestCommit) return { name: branch.name, updatedAt: 0, rank: branch.rank };
|
|
55
|
+
try {
|
|
56
|
+
const response = await httpClient.get(`${base}/commits/${encodeURIComponent(branch.latestCommit)}`, { headers: bitbucketHeaders() });
|
|
57
|
+
return { name: branch.name, updatedAt: response.status >= 200 && response.status < 300 ? branchTimestamp(response.data) : 0, rank: branch.rank };
|
|
58
|
+
} catch {
|
|
59
|
+
return { name: branch.name, updatedAt: 0, rank: branch.rank };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
27
62
|
function updateTask(taskId, updater) {
|
|
28
63
|
const data = readPackageUpgradeData(); const task = data.tasks.find((item) => item.id === taskId);
|
|
29
64
|
if (!task) return;
|
|
@@ -82,7 +117,7 @@ async function runRepo(task, repo) {
|
|
|
82
117
|
if (!parsed) throw new Error('Could not parse Bitbucket repository URL');
|
|
83
118
|
const headers = { Accept: 'application/json', 'Content-Type': 'application/json' }; if (settings.bitbucketAccessToken) headers.Authorization = `Bearer ${settings.bitbucketAccessToken}`;
|
|
84
119
|
const api = `${parsed.protocol}://${parsed.host}/rest/api/latest/projects/${encodeURIComponent(parsed.project)}/repos/${encodeURIComponent(parsed.slug)}/pull-requests`;
|
|
85
|
-
const response = await
|
|
120
|
+
const response = await httpClient.post(api, { title: task.commitMessage, description: `Upgrade ${task.packageName} to ${task.version}`, fromRef: { id: `refs/heads/${task.sourceBranch}` }, toRef: { id: `refs/heads/${task.targetBranch}` } }, { headers, validateStatus: () => true });
|
|
86
121
|
if (response.status < 200 || response.status >= 300) throw new Error(response.data?.errors?.[0]?.message || `Bitbucket PR failed (${response.status})`);
|
|
87
122
|
const prUrl = response.data?.links?.self?.[0]?.href || `${parsed.protocol}://${parsed.host}/projects/${parsed.project}/repos/${parsed.slug}/pull-requests/${response.data.id}`;
|
|
88
123
|
updateTask(task.id, (t) => ({ ...t, repos: t.repos.map((r) => r.repoId === repo.id ? { ...result, status: 'success', prUrl } : r) }));
|
|
@@ -117,6 +152,26 @@ router.post('/repos/packages', (req, res) => {
|
|
|
117
152
|
});
|
|
118
153
|
res.json([...packageNames].sort((a, b) => a.localeCompare(b)));
|
|
119
154
|
});
|
|
155
|
+
router.post('/repos/branches', async (req, res) => {
|
|
156
|
+
const data = readPackageUpgradeData();
|
|
157
|
+
const repoIds = Array.isArray(req.body?.repoIds) ? req.body.repoIds : [];
|
|
158
|
+
const repos = repoIds.map((repoId) => data.repos.find((repo) => repo.id === repoId)).filter(Boolean);
|
|
159
|
+
if (!repos.length) return res.json([]);
|
|
160
|
+
try {
|
|
161
|
+
const remote = await Promise.all(repos.map((repo) => fetchRemoteBranches(repo)));
|
|
162
|
+
const branchSets = remote.map(({ branches }) => new Set(branches.map((branch) => branch.name)));
|
|
163
|
+
const commonNames = remote[0].branches.map((branch) => branch.name).filter((name) => branchSets.every((branches) => branches.has(name)));
|
|
164
|
+
const commonBranches = await Promise.all(commonNames.map(async (name) => {
|
|
165
|
+
const branches = remote.map(({ base, branches: items }) => ({ base, branch: items.find((item) => item.name === name) })).filter(({ branch }) => branch);
|
|
166
|
+
const updated = await Promise.all(branches.map(({ base, branch }) => fetchBranchUpdatedAt(base, branch)));
|
|
167
|
+
return { name, updatedAt: Math.max(...updated.map((item) => item.updatedAt)), rank: Math.min(...updated.map((item) => item.rank)) };
|
|
168
|
+
}));
|
|
169
|
+
commonBranches.sort((a, b) => b.updatedAt - a.updatedAt || a.rank - b.rank || a.name.localeCompare(b.name));
|
|
170
|
+
res.json(commonBranches.map(({ name, updatedAt }) => ({ name, updatedAt: updatedAt ? new Date(updatedAt).toISOString() : null })));
|
|
171
|
+
} catch (error) {
|
|
172
|
+
res.status(502).json({ error: error.message || 'Failed to fetch common repository branches.' });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
120
175
|
router.post('/repos/:id/clone', async (req, res) => { const data = readPackageUpgradeData(); const repo = data.repos.find((item) => item.id === req.params.id); if (!repo) return res.status(404).json({ error: 'Repository not found.' }); if (!data.workspaceDir) return res.status(400).json({ error: 'Configure a workspace directory first.' }); const folder = repoPath(data, repo); try { mkdirSync(data.workspaceDir, { recursive: true }); if (!existsSync(join(folder, '.git'))) await exec('git', ['clone', repo.url, folder], { cwd: data.workspaceDir, timeout: 20 * 60 * 1000, maxBuffer: 2 * 1024 * 1024 }); res.json({ ...repo, cloned: true }); } catch (error) { res.status(400).json({ error: error.stderr || error.message }); } });
|
|
121
176
|
router.delete('/repos/:id', (req, res) => { const data = readPackageUpgradeData(); data.repos = data.repos.filter((r) => r.id !== req.params.id); savePackageUpgradeData(data); res.status(204).end(); });
|
|
122
177
|
router.post('/tasks', (req, res) => { const body = req.body || {}; const required = ['sourceBranch', 'targetBranch', 'packageName', 'version', 'commitMessage']; if (required.some((key) => !String(body[key] || '').trim()) || !Array.isArray(body.repoIds) || !body.repoIds.length) return res.status(400).json({ error: 'All fields and at least one repository are required.' }); const data = readPackageUpgradeData(); if (!data.workspaceDir) return res.status(400).json({ error: 'Configure a workspace directory first.' }); const repoIds = body.repoIds.filter((id) => data.repos.some((repo) => repo.id === id)); if (!repoIds.length) return res.status(400).json({ error: 'Select at least one configured repository.' }); const task = { id: `task-${Date.now()}`, ...Object.fromEntries(required.map((key) => [key, String(body[key]).trim()])), repoIds, repos: repoIds.map((repoId) => ({ repoId, status: 'queued' })), status: 'running', createdAt: new Date().toISOString() }; data.tasks.unshift(task); savePackageUpgradeData(data); void execute(task); res.status(201).json(task); });
|
|
@@ -138,6 +138,27 @@ function getEditorUrl(editor = 'vscode', path = '') {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
function dayItems(date) { return readJson(dayFile(date), []); }
|
|
141
|
+
|
|
142
|
+
async function getClipboardText() {
|
|
143
|
+
const commands = process.platform === 'darwin'
|
|
144
|
+
? [['pbpaste', []]]
|
|
145
|
+
: process.platform === 'win32'
|
|
146
|
+
? [['powershell.exe', ['-NoProfile', '-Command', 'Get-Clipboard -Format Text -Raw']]]
|
|
147
|
+
: [
|
|
148
|
+
['wl-paste', ['--no-newline', '--type', 'text']],
|
|
149
|
+
['xclip', ['-selection', 'clipboard', '-o', '-t', 'UTF8_STRING']],
|
|
150
|
+
['xsel', ['--clipboard', '--output']]
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
for (const [command, args] of commands) {
|
|
154
|
+
try {
|
|
155
|
+
const { stdout } = await execFileAsync(command, args, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 });
|
|
156
|
+
return stdout;
|
|
157
|
+
} catch {}
|
|
158
|
+
}
|
|
159
|
+
return '';
|
|
160
|
+
}
|
|
161
|
+
|
|
141
162
|
export function clipboardItems(date = today()) {
|
|
142
163
|
const settings = readSettings();
|
|
143
164
|
const editor = settings.defaultEditor || 'vscode';
|
|
@@ -208,7 +229,7 @@ export function deleteClipboardItem(date, id) {
|
|
|
208
229
|
const contentPath = join(paths.clipboardDir, 'content', item.contentFile);
|
|
209
230
|
if (existsSync(contentPath)) unlinkSync(contentPath);
|
|
210
231
|
}
|
|
211
|
-
if (item.imageFile) {
|
|
232
|
+
if (item.imageFile && process.platform === 'darwin') {
|
|
212
233
|
const imagePath = join(paths.clipboardDir, 'content', item.imageFile);
|
|
213
234
|
if (existsSync(imagePath)) unlinkSync(imagePath);
|
|
214
235
|
}
|
|
@@ -216,13 +237,12 @@ export function deleteClipboardItem(date, id) {
|
|
|
216
237
|
}
|
|
217
238
|
|
|
218
239
|
export async function captureClipboard() {
|
|
219
|
-
if (process.platform !== 'darwin') return;
|
|
220
240
|
const settings = readSettings();
|
|
221
241
|
if (settings.clipboardEnabled === false) return;
|
|
222
242
|
|
|
223
243
|
// 1. Text capture
|
|
224
244
|
try {
|
|
225
|
-
const
|
|
245
|
+
const stdout = await getClipboardText();
|
|
226
246
|
const text = stdout.trim();
|
|
227
247
|
if (text && text !== lastValue) {
|
|
228
248
|
lastValue = text;
|
|
@@ -277,7 +297,7 @@ export async function captureClipboard() {
|
|
|
277
297
|
} catch {}
|
|
278
298
|
|
|
279
299
|
// 2. Image capture (Mac only & clipboardImageEnabled !== false)
|
|
280
|
-
if (settings.clipboardImageEnabled !== false) {
|
|
300
|
+
if (process.platform === 'darwin' && settings.clipboardImageEnabled !== false) {
|
|
281
301
|
try {
|
|
282
302
|
const imageData = await getMacClipboardImageData();
|
|
283
303
|
if (imageData && imageData.rgbaBuf.length > 0) {
|