buddy-workbench 0.1.40 → 0.1.42
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 +19 -24
- package/pages/welcome/index.html +44 -9
- package/server/config.js +2 -0
- package/server/repositories/jira-filters.js +29 -0
- package/server/routes/jira-filters.js +86 -3
- package/server/routes/package-upgrade.js +48 -3
- package/server/routes/updates.js +29 -0
- package/ui/dist/assets/index-43tnCAgi.css +1 -0
- package/ui/dist/assets/index-CE_oDDqW.js +547 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/sw.js +2 -2
- package/ui/dist/assets/index-BWhse591.css +0 -1
- package/ui/dist/assets/index-EnuwkosA.js +0 -547
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "buddy-workbench",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.42",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,38 +25,33 @@
|
|
|
25
25
|
},
|
|
26
26
|
"devbuddyChangelog": [
|
|
27
27
|
{
|
|
28
|
-
"version": "0.1.
|
|
29
|
-
"name": "
|
|
30
|
-
"notes": [
|
|
31
|
-
"Added system-level NPM, Git, and NVM configuration management.",
|
|
32
|
-
"Added NVM detection, Node.js version installation, download mirror configuration, and current runtime details.",
|
|
33
|
-
"Added global NPM package search, version selection with publish dates, install confirmation, upgrade, and uninstall actions."
|
|
34
|
-
]
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
"version": "0.1.28",
|
|
38
|
-
"name": "Presentation planning and presenter tools",
|
|
28
|
+
"version": "0.1.42",
|
|
29
|
+
"name": "Jira Workspace and navigation improvements",
|
|
39
30
|
"notes": [
|
|
40
|
-
"Added
|
|
41
|
-
"Added
|
|
42
|
-
"
|
|
31
|
+
"Added Jira Workspace template management with Filter List and Template List tabs, including Jira summary lookup, editing, deletion, and clickable Jira No links.",
|
|
32
|
+
"Added +Jira creation with template selection, saved summary defaults, cloning, self-assignment, and Recently Created history with 30-item pagination.",
|
|
33
|
+
"Renamed Jira Filters to Jira Workspace and renamed To-Do List to To-Do, moving it to the first position under Plan & Review.",
|
|
34
|
+
"Changed Filter creation and editing to inline forms and improved Recently Created layout, summary truncation, and timestamp formatting."
|
|
43
35
|
]
|
|
44
36
|
},
|
|
45
37
|
{
|
|
46
|
-
"version": "0.1.
|
|
47
|
-
"name": "
|
|
38
|
+
"version": "0.1.41",
|
|
39
|
+
"name": "Workflow improvements and theme-aware demo pages",
|
|
48
40
|
"notes": [
|
|
49
|
-
"Added
|
|
50
|
-
"
|
|
41
|
+
"Added folder selection and system-folder opening for NPM Prefix and Cache settings, with loading feedback while system configuration loads.",
|
|
42
|
+
"Improved Package Upgrade tasks with repository-based dependency suggestions, npm version lookup with cascading release selection, multi-package-manager lockfile support, and clearer branch and task history workflows.",
|
|
43
|
+
"Added a Check all action to Branch Sync Status without automatically checking on page entry, and improved task detail viewing.",
|
|
44
|
+
"Added a one-click Static Pages test demo and made the Welcome demo page adapt to DevBuddy light and dark themes.",
|
|
45
|
+
"Improved Clipboard History keyboard focus behavior so date navigation remains available after interacting with category tabs."
|
|
51
46
|
]
|
|
52
47
|
},
|
|
53
48
|
{
|
|
54
|
-
"version": "0.1.
|
|
55
|
-
"name": "
|
|
49
|
+
"version": "0.1.33",
|
|
50
|
+
"name": "Developer configuration management",
|
|
56
51
|
"notes": [
|
|
57
|
-
"Added
|
|
58
|
-
"
|
|
59
|
-
"Added
|
|
52
|
+
"Added system-level NPM, Git, and NVM configuration management.",
|
|
53
|
+
"Added NVM detection, Node.js version installation, download mirror configuration, and current runtime details.",
|
|
54
|
+
"Added global NPM package search, version selection with publish dates, install confirmation, upgrade, and uninstall actions."
|
|
60
55
|
]
|
|
61
56
|
}
|
|
62
57
|
],
|
package/pages/welcome/index.html
CHANGED
|
@@ -4,14 +4,47 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Welcome - DevBuddy Static Pages</title>
|
|
7
|
+
<script>
|
|
8
|
+
(() => {
|
|
9
|
+
const applyTheme = (theme) => {
|
|
10
|
+
document.documentElement.dataset.theme = theme;
|
|
11
|
+
};
|
|
12
|
+
let savedTheme = '';
|
|
13
|
+
try { savedTheme = localStorage.getItem('devbuddy-theme') || ''; } catch {}
|
|
14
|
+
applyTheme(savedTheme === 'dark' || savedTheme === 'light'
|
|
15
|
+
? savedTheme
|
|
16
|
+
: (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'));
|
|
17
|
+
window.addEventListener('storage', (event) => {
|
|
18
|
+
if (event.key === 'devbuddy-theme' && (event.newValue === 'dark' || event.newValue === 'light')) {
|
|
19
|
+
applyTheme(event.newValue);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
})();
|
|
23
|
+
</script>
|
|
7
24
|
<style>
|
|
8
25
|
:root {
|
|
9
|
-
|
|
10
|
-
--
|
|
11
|
-
--
|
|
12
|
-
--text
|
|
13
|
-
--
|
|
14
|
-
--
|
|
26
|
+
color-scheme: light;
|
|
27
|
+
--bg: #f7f8fc;
|
|
28
|
+
--card-bg: #ffffff;
|
|
29
|
+
--text: #172033;
|
|
30
|
+
--text-muted: #667085;
|
|
31
|
+
--accent: #5b50e0;
|
|
32
|
+
--border: #e5e8f0;
|
|
33
|
+
--code-bg: #f0f1f8;
|
|
34
|
+
--code-text: #cf3038;
|
|
35
|
+
--shadow: 0 18px 44px rgba(31, 42, 68, .085);
|
|
36
|
+
}
|
|
37
|
+
:root[data-theme="dark"] {
|
|
38
|
+
color-scheme: dark;
|
|
39
|
+
--bg: #0f1117;
|
|
40
|
+
--card-bg: #171a22;
|
|
41
|
+
--text: #e7eaf0;
|
|
42
|
+
--text-muted: #929aaa;
|
|
43
|
+
--accent: #7669ee;
|
|
44
|
+
--border: #2a3040;
|
|
45
|
+
--code-bg: #101522;
|
|
46
|
+
--code-text: #ff8f9a;
|
|
47
|
+
--shadow: 0 18px 44px rgba(0, 0, 0, .28);
|
|
15
48
|
}
|
|
16
49
|
body {
|
|
17
50
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
@@ -24,16 +57,18 @@
|
|
|
24
57
|
margin: 0;
|
|
25
58
|
padding: 24px;
|
|
26
59
|
box-sizing: border-box;
|
|
60
|
+
transition: background-color .2s ease, color .2s ease;
|
|
27
61
|
}
|
|
28
62
|
.card {
|
|
29
63
|
background: var(--card-bg);
|
|
30
64
|
padding: 32px;
|
|
31
65
|
border-radius: 16px;
|
|
32
|
-
box-shadow:
|
|
66
|
+
box-shadow: var(--shadow);
|
|
33
67
|
border: 1px solid var(--border);
|
|
34
68
|
text-align: center;
|
|
35
69
|
max-width: 520px;
|
|
36
70
|
width: 100%;
|
|
71
|
+
transition: background-color .2s ease, border-color .2s ease, box-shadow .2s ease;
|
|
37
72
|
}
|
|
38
73
|
.icon {
|
|
39
74
|
font-size: 48px;
|
|
@@ -51,10 +86,10 @@
|
|
|
51
86
|
margin-bottom: 20px;
|
|
52
87
|
}
|
|
53
88
|
code {
|
|
54
|
-
background:
|
|
89
|
+
background: var(--code-bg);
|
|
55
90
|
padding: 4px 8px;
|
|
56
91
|
border-radius: 6px;
|
|
57
|
-
color:
|
|
92
|
+
color: var(--code-text);
|
|
58
93
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
59
94
|
font-size: 0.9em;
|
|
60
95
|
border: 1px solid var(--border);
|
package/server/config.js
CHANGED
|
@@ -26,6 +26,8 @@ export const paths = {
|
|
|
26
26
|
groupTasks: join(userDataDir, 'group-tasks.json'),
|
|
27
27
|
settings: join(userDataDir, 'settings.json'),
|
|
28
28
|
jiraFilters: join(userDataDir, 'jira-filters.json'),
|
|
29
|
+
jiraTemplates: join(userDataDir, 'jira-templates.json'),
|
|
30
|
+
jiraRecentlyCreated: join(userDataDir, 'jira-recently-created.json'),
|
|
29
31
|
todos: join(userDataDir, 'todos.json'),
|
|
30
32
|
staticPages: join(userDataDir, 'static-pages.json'),
|
|
31
33
|
errors: join(userDataDir, 'errors.json'),
|
|
@@ -14,3 +14,32 @@ export function saveJiraFilters(filters) {
|
|
|
14
14
|
mkdirSync(dirname(paths.jiraFilters), { recursive: true });
|
|
15
15
|
writeFileSync(paths.jiraFilters, JSON.stringify(filters, null, 2));
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
function readList(path) {
|
|
19
|
+
try {
|
|
20
|
+
return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : [];
|
|
21
|
+
} catch {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function saveList(path, items) {
|
|
27
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
28
|
+
writeFileSync(path, JSON.stringify(items, null, 2));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function listJiraTemplates() {
|
|
32
|
+
return readList(paths.jiraTemplates);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function saveJiraTemplates(templates) {
|
|
36
|
+
saveList(paths.jiraTemplates, templates);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function listRecentlyCreatedJiraIssues() {
|
|
40
|
+
return readList(paths.jiraRecentlyCreated);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function saveRecentlyCreatedJiraIssues(issues) {
|
|
44
|
+
saveList(paths.jiraRecentlyCreated, issues);
|
|
45
|
+
}
|
|
@@ -2,7 +2,7 @@ import https from 'node:https';
|
|
|
2
2
|
import axios from 'axios';
|
|
3
3
|
import { Router } from 'express';
|
|
4
4
|
import { readSettings } from '../repositories/settings.js';
|
|
5
|
-
import { listJiraFilters, saveJiraFilters } from '../repositories/jira-filters.js';
|
|
5
|
+
import { listJiraFilters, saveJiraFilters, listJiraTemplates, saveJiraTemplates, listRecentlyCreatedJiraIssues, saveRecentlyCreatedJiraIssues } from '../repositories/jira-filters.js';
|
|
6
6
|
|
|
7
7
|
const router = Router();
|
|
8
8
|
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
@@ -35,11 +35,79 @@ function getPriorityRank(priority) {
|
|
|
35
35
|
return 0;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
async function getJiraIssueSummary(issueKey) {
|
|
39
|
+
const jiraHost = getJiraHost();
|
|
40
|
+
if (!jiraHost) throw new Error('Jira domain is not configured. Please set Domain in Settings.');
|
|
41
|
+
const settings = readSettings();
|
|
42
|
+
const token = settings.jiraAccessToken;
|
|
43
|
+
const headers = {};
|
|
44
|
+
if (token) headers.Authorization = token.startsWith('Bearer ') ? token : `Bearer ${token}`;
|
|
45
|
+
const url = `https://${jiraHost}/rest/api/2/issue/${encodeURIComponent(issueKey)}?fields=summary`;
|
|
46
|
+
const response = await httpClient.get(url, { headers });
|
|
47
|
+
if (response.status !== 200) {
|
|
48
|
+
const errMsg = response.data?.errorMessages?.[0] || response.data?.message || `Jira API returned status ${response.status}`;
|
|
49
|
+
throw new Error(errMsg);
|
|
50
|
+
}
|
|
51
|
+
const summary = String(response.data?.fields?.summary || '').trim();
|
|
52
|
+
if (!summary) throw new Error(`Jira issue ${issueKey} has no summary.`);
|
|
53
|
+
return summary;
|
|
54
|
+
}
|
|
55
|
+
|
|
38
56
|
// List all filters
|
|
39
57
|
router.get('/', (_req, res) => {
|
|
40
58
|
res.json(listJiraFilters());
|
|
41
59
|
});
|
|
42
60
|
|
|
61
|
+
router.get('/templates', (_req, res) => {
|
|
62
|
+
res.json(listJiraTemplates());
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
router.post('/templates', async (req, res) => {
|
|
66
|
+
const name = String(req.body?.name || '').trim();
|
|
67
|
+
const jiraNo = String(req.body?.jiraNo || '').trim();
|
|
68
|
+
if (!name) return res.status(400).json({ error: 'Template name is required.' });
|
|
69
|
+
if (!jiraNo) return res.status(400).json({ error: 'Jira No is required.' });
|
|
70
|
+
try {
|
|
71
|
+
const summary = await getJiraIssueSummary(jiraNo);
|
|
72
|
+
const templates = listJiraTemplates();
|
|
73
|
+
const template = { id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6), name, jiraNo, summary, createdAt: new Date().toISOString() };
|
|
74
|
+
templates.unshift(template);
|
|
75
|
+
saveJiraTemplates(templates);
|
|
76
|
+
res.status(201).json(template);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
res.status(400).json({ error: error.message || 'Failed to read Jira issue summary.' });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
router.put('/templates/:id', async (req, res) => {
|
|
83
|
+
const templates = listJiraTemplates();
|
|
84
|
+
const index = templates.findIndex((template) => template.id === req.params.id);
|
|
85
|
+
if (index === -1) return res.status(404).json({ error: 'Template not found.' });
|
|
86
|
+
const name = String(req.body?.name || '').trim();
|
|
87
|
+
const jiraNo = String(req.body?.jiraNo || '').trim();
|
|
88
|
+
if (!name || !jiraNo) return res.status(400).json({ error: 'Template name and Jira No are required.' });
|
|
89
|
+
try {
|
|
90
|
+
const summary = await getJiraIssueSummary(jiraNo);
|
|
91
|
+
templates[index] = { ...templates[index], name, jiraNo, summary, updatedAt: new Date().toISOString() };
|
|
92
|
+
saveJiraTemplates(templates);
|
|
93
|
+
res.json(templates[index]);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
res.status(400).json({ error: error.message || 'Failed to read Jira issue summary.' });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
router.delete('/templates/:id', (req, res) => {
|
|
100
|
+
const templates = listJiraTemplates();
|
|
101
|
+
const next = templates.filter((template) => template.id !== req.params.id);
|
|
102
|
+
if (next.length === templates.length) return res.status(404).json({ error: 'Template not found.' });
|
|
103
|
+
saveJiraTemplates(next);
|
|
104
|
+
res.status(204).end();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
router.get('/recently-created', (_req, res) => {
|
|
108
|
+
res.json(listRecentlyCreatedJiraIssues());
|
|
109
|
+
});
|
|
110
|
+
|
|
43
111
|
// Create filter
|
|
44
112
|
router.post('/', (req, res) => {
|
|
45
113
|
const { name, filterId } = req.body || {};
|
|
@@ -165,7 +233,7 @@ router.get('/:id/issues', async (req, res) => {
|
|
|
165
233
|
|
|
166
234
|
// Clone an issue
|
|
167
235
|
router.post('/issues/clone', async (req, res) => {
|
|
168
|
-
const { issueKey, summary, issueType } = req.body || {};
|
|
236
|
+
const { issueKey, summary, issueType, templateName, trackRecent } = req.body || {};
|
|
169
237
|
if (!summary || typeof summary !== 'string' || !summary.trim()) {
|
|
170
238
|
return res.status(400).json({ error: 'Summary is required for clone.' });
|
|
171
239
|
}
|
|
@@ -287,7 +355,22 @@ router.post('/issues/clone', async (req, res) => {
|
|
|
287
355
|
}
|
|
288
356
|
}
|
|
289
357
|
|
|
290
|
-
|
|
358
|
+
const issueUrl = `https://${jiraHost}/browse/${createdKey}`;
|
|
359
|
+
if (trackRecent) {
|
|
360
|
+
const recentIssue = {
|
|
361
|
+
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
|
362
|
+
key: createdKey,
|
|
363
|
+
jiraId: created.id,
|
|
364
|
+
summary: summary.trim(),
|
|
365
|
+
templateName: templateName || issueKey,
|
|
366
|
+
url: issueUrl,
|
|
367
|
+
createdAt: new Date().toISOString()
|
|
368
|
+
};
|
|
369
|
+
const recentIssues = listRecentlyCreatedJiraIssues().filter((item) => item.key !== createdKey);
|
|
370
|
+
recentIssues.unshift(recentIssue);
|
|
371
|
+
saveRecentlyCreatedJiraIssues(recentIssues.slice(0, 50));
|
|
372
|
+
}
|
|
373
|
+
res.status(201).json({ success: true, issue: { key: createdKey, id: created.id, url: issueUrl } });
|
|
291
374
|
} catch (error) {
|
|
292
375
|
res.status(500).json({ error: error.message || 'Failed to clone Jira issue.' });
|
|
293
376
|
}
|
|
@@ -29,6 +29,25 @@ function updateTask(taskId, updater) {
|
|
|
29
29
|
const next = updater(task); data.tasks = data.tasks.map((item) => item.id === taskId ? next : item); savePackageUpgradeData(data);
|
|
30
30
|
}
|
|
31
31
|
async function git(folder, args) { return (await exec('git', args, { cwd: folder, timeout: 10 * 60 * 1000, maxBuffer: 2 * 1024 * 1024 })).stdout.trim(); }
|
|
32
|
+
async function gitRefExists(folder, ref) {
|
|
33
|
+
try {
|
|
34
|
+
await exec('git', ['show-ref', '--verify', '--quiet', ref], { cwd: folder, timeout: 30 * 1000 });
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function updateDependencyLockfile(folder) {
|
|
41
|
+
const lockfile = existsSync(join(folder, 'pnpm-lock.yaml')) ? 'pnpm-lock.yaml' : existsSync(join(folder, 'yarn.lock')) ? 'yarn.lock' : 'package-lock.json';
|
|
42
|
+
const packageManager = lockfile === 'pnpm-lock.yaml' ? 'pnpm' : lockfile === 'yarn.lock' ? 'yarn' : 'npm';
|
|
43
|
+
const commands = packageManager === 'pnpm'
|
|
44
|
+
? ['pnpm', ['install', '--lockfile-only', '--ignore-scripts']]
|
|
45
|
+
: packageManager === 'yarn'
|
|
46
|
+
? ['yarn', ['install', '--ignore-scripts']]
|
|
47
|
+
: ['npm', ['install', '--legacy-peer-deps', '--package-lock-only', '--ignore-scripts']];
|
|
48
|
+
await exec(commands[0], commands[1], { cwd: folder, timeout: 20 * 60 * 1000, maxBuffer: 4 * 1024 * 1024 });
|
|
49
|
+
return lockfile;
|
|
50
|
+
}
|
|
32
51
|
async function runRepo(task, repo) {
|
|
33
52
|
const data = readPackageUpgradeData(); const folder = repoPath(data, repo); const name = repo.name || repoName(repo.url);
|
|
34
53
|
const result = { repoId: repo.id, name, status: 'running', message: '', prUrl: null };
|
|
@@ -37,7 +56,15 @@ async function runRepo(task, repo) {
|
|
|
37
56
|
mkdirSync(data.workspaceDir, { recursive: true });
|
|
38
57
|
if (!existsSync(join(folder, '.git'))) await exec('git', ['clone', repo.url, folder], { cwd: data.workspaceDir, timeout: 20 * 60 * 1000, maxBuffer: 2 * 1024 * 1024 });
|
|
39
58
|
await git(folder, ['fetch', 'origin', '--prune']);
|
|
40
|
-
|
|
59
|
+
const remoteTargetRef = `refs/remotes/origin/${task.targetBranch}`;
|
|
60
|
+
const localTargetRef = `refs/heads/${task.targetBranch}`;
|
|
61
|
+
const targetRef = await gitRefExists(folder, remoteTargetRef)
|
|
62
|
+
? `origin/${task.targetBranch}`
|
|
63
|
+
: await gitRefExists(folder, localTargetRef)
|
|
64
|
+
? task.targetBranch
|
|
65
|
+
: null;
|
|
66
|
+
if (!targetRef) throw new Error(`Target branch "${task.targetBranch}" was not found in ${name}. Check the target branch name and confirm it exists on origin.`);
|
|
67
|
+
await git(folder, ['checkout', '-B', task.sourceBranch, targetRef]);
|
|
41
68
|
const packageFile = join(folder, 'package.json');
|
|
42
69
|
if (!existsSync(packageFile)) throw new Error('package.json not found');
|
|
43
70
|
const pkg = JSON.parse(readFileSync(packageFile, 'utf8'));
|
|
@@ -46,8 +73,8 @@ async function runRepo(task, repo) {
|
|
|
46
73
|
if (!section) throw new Error(`Package ${task.packageName} is not in package.json`);
|
|
47
74
|
pkg[section][task.packageName] = task.version;
|
|
48
75
|
writeFileSync(packageFile, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
49
|
-
|
|
50
|
-
await git(folder, ['add', 'package.json',
|
|
76
|
+
const lockfile = await updateDependencyLockfile(folder);
|
|
77
|
+
await git(folder, ['add', 'package.json', lockfile]);
|
|
51
78
|
await git(folder, ['commit', '-m', task.commitMessage]);
|
|
52
79
|
await git(folder, ['push', 'origin', task.sourceBranch]);
|
|
53
80
|
const parsed = parseBitbucket(repo.url); const settings = readSettings();
|
|
@@ -70,6 +97,24 @@ async function execute(task) {
|
|
|
70
97
|
router.get('/config', (_req, res) => { const data = readPackageUpgradeData(); res.json({ workspaceDir: data.workspaceDir, repos: data.repos.map((repo) => ({ ...repo, cloned: existsSync(join(repoPath(data, repo), '.git')) })), tasks: data.tasks }); });
|
|
71
98
|
router.put('/config', (req, res) => { const data = readPackageUpgradeData(); data.workspaceDir = String(req.body?.workspaceDir || '').trim(); if (!data.workspaceDir) return res.status(400).json({ error: 'Workspace directory is required.' }); savePackageUpgradeData(data); res.json({ workspaceDir: data.workspaceDir, repos: data.repos }); });
|
|
72
99
|
router.post('/repos', (req, res) => { const url = String(req.body?.url || '').trim(); if (!url) return res.status(400).json({ error: 'Repository URL is required.' }); const data = readPackageUpgradeData(); const repo = { id: `repo-${Date.now()}`, url, name: String(req.body?.name || repoName(url)), directory: String(req.body?.directory || repoName(url)) }; data.repos.push(repo); savePackageUpgradeData(data); res.status(201).json(repo); });
|
|
100
|
+
router.post('/repos/packages', (req, res) => {
|
|
101
|
+
const data = readPackageUpgradeData();
|
|
102
|
+
const repoIds = Array.isArray(req.body?.repoIds) ? req.body.repoIds : [];
|
|
103
|
+
const packageNames = new Set();
|
|
104
|
+
repoIds.forEach((repoId) => {
|
|
105
|
+
const repo = data.repos.find((item) => item.id === repoId);
|
|
106
|
+
if (!repo) return;
|
|
107
|
+
const packageFile = join(repoPath(data, repo), 'package.json');
|
|
108
|
+
if (!existsSync(packageFile)) return;
|
|
109
|
+
try {
|
|
110
|
+
const packageJson = JSON.parse(readFileSync(packageFile, 'utf8'));
|
|
111
|
+
['dependencies', 'devDependencies'].forEach((section) => {
|
|
112
|
+
Object.keys(packageJson[section] || {}).forEach((name) => packageNames.add(name));
|
|
113
|
+
});
|
|
114
|
+
} catch {}
|
|
115
|
+
});
|
|
116
|
+
res.json([...packageNames].sort((a, b) => a.localeCompare(b)));
|
|
117
|
+
});
|
|
73
118
|
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 }); } });
|
|
74
119
|
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(); });
|
|
75
120
|
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); });
|
package/server/routes/updates.js
CHANGED
|
@@ -14,6 +14,7 @@ const CACHE_TTL_MS = 15 * 60 * 1000;
|
|
|
14
14
|
|
|
15
15
|
let cachedResult = null;
|
|
16
16
|
let cachedAt = 0;
|
|
17
|
+
let selfUpdateRunning = false;
|
|
17
18
|
|
|
18
19
|
function versionParts(version) {
|
|
19
20
|
return String(version || '')
|
|
@@ -137,4 +138,32 @@ router.get('/', async (req, res) => {
|
|
|
137
138
|
return res.json(cachedResult);
|
|
138
139
|
});
|
|
139
140
|
|
|
141
|
+
router.post('/self-update', async (_req, res) => {
|
|
142
|
+
if (selfUpdateRunning) {
|
|
143
|
+
return res.status(409).json({ error: 'An update is already in progress.' });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
selfUpdateRunning = true;
|
|
147
|
+
try {
|
|
148
|
+
const { stdout } = await execFileAsync(
|
|
149
|
+
process.execPath,
|
|
150
|
+
[join(root, 'bin', 'devbuddy.js'), 'self-update'],
|
|
151
|
+
{
|
|
152
|
+
cwd: root,
|
|
153
|
+
timeout: 120000,
|
|
154
|
+
maxBuffer: 1024 * 1024,
|
|
155
|
+
windowsHide: true
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
cachedResult = null;
|
|
159
|
+
cachedAt = 0;
|
|
160
|
+
return res.json({ success: true, message: stdout.trim() || 'DevBuddy was updated successfully.' });
|
|
161
|
+
} catch (error) {
|
|
162
|
+
const details = error?.stderr?.trim() || error?.stdout?.trim() || error?.message;
|
|
163
|
+
return res.status(500).json({ error: details || 'DevBuddy could not be updated.' });
|
|
164
|
+
} finally {
|
|
165
|
+
selfUpdateRunning = false;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
140
169
|
export default router;
|