buddy-workbench 0.1.8 → 0.1.10

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.8",
3
+ "version": "0.1.10",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,6 +20,7 @@
20
20
  "prepublishOnly": "npm run build"
21
21
  },
22
22
  "dependencies": {
23
+ "axios": "^1.7.9",
23
24
  "express": "^5.1.0"
24
25
  }
25
26
  }
package/server/config.js CHANGED
@@ -7,6 +7,9 @@ export const paths = {
7
7
  portHistory: join(root, 'data', 'port-history.json'),
8
8
  groupTasks: join(root, 'data', 'group-tasks.json'),
9
9
  settings: join(root, 'data', 'settings.json'),
10
+ jiraFilters: join(root, 'data', 'jira-filters.json'),
11
+ todos: join(root, 'data', 'todos.json'),
12
+ staticPages: join(root, 'data', 'static-pages.json'),
10
13
  shutdownLog: join(root, 'data', 'shutdown.log'),
11
14
  clipboardDir: join(root, 'data', 'clipboard'),
12
15
  plugins: join(root, 'plugins'),
@@ -0,0 +1,16 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { paths } from '../config.js';
4
+
5
+ export function listJiraFilters() {
6
+ try {
7
+ return existsSync(paths.jiraFilters) ? JSON.parse(readFileSync(paths.jiraFilters, 'utf8')) : [];
8
+ } catch {
9
+ return [];
10
+ }
11
+ }
12
+
13
+ export function saveJiraFilters(filters) {
14
+ mkdirSync(dirname(paths.jiraFilters), { recursive: true });
15
+ writeFileSync(paths.jiraFilters, JSON.stringify(filters, null, 2));
16
+ }
@@ -11,6 +11,9 @@ export function settingsStatus() {
11
11
  const settings = readSettings();
12
12
  return {
13
13
  domain: typeof settings.domain === 'string' ? settings.domain : '',
14
+ jiraIssuePrefix: typeof settings.jiraIssuePrefix === 'string' ? settings.jiraIssuePrefix : '',
15
+ defaultEditor: typeof settings.defaultEditor === 'string' ? settings.defaultEditor : 'vscode',
16
+ defaultBrowser: typeof settings.defaultBrowser === 'string' ? settings.defaultBrowser : 'chrome',
14
17
  bitbucketTokenConfigured: Boolean(settings.bitbucketAccessToken),
15
18
  jiraTokenConfigured: Boolean(settings.jiraAccessToken),
16
19
  confluenceTokenConfigured: Boolean(settings.confluenceAccessToken),
@@ -27,6 +30,33 @@ export function saveDomain(domain) {
27
30
  return settingsStatus();
28
31
  }
29
32
 
33
+ export function saveJiraIssuePrefix(jiraIssuePrefix) {
34
+ const settings = readSettings();
35
+ if (jiraIssuePrefix) settings.jiraIssuePrefix = jiraIssuePrefix;
36
+ else delete settings.jiraIssuePrefix;
37
+ mkdirSync(dirname(paths.settings), { recursive: true });
38
+ writeFileSync(paths.settings, JSON.stringify(settings, null, 2), { mode: 0o600 });
39
+ return settingsStatus();
40
+ }
41
+
42
+ export function saveDefaultEditor(defaultEditor) {
43
+ const settings = readSettings();
44
+ const valid = ['vscode', 'devin', 'idea'];
45
+ settings.defaultEditor = valid.includes(defaultEditor) ? defaultEditor : 'vscode';
46
+ mkdirSync(dirname(paths.settings), { recursive: true });
47
+ writeFileSync(paths.settings, JSON.stringify(settings, null, 2), { mode: 0o600 });
48
+ return settingsStatus();
49
+ }
50
+
51
+ export function saveDefaultBrowser(defaultBrowser) {
52
+ const settings = readSettings();
53
+ const valid = ['chrome', 'edge', 'safari'];
54
+ settings.defaultBrowser = valid.includes(defaultBrowser) ? defaultBrowser : 'chrome';
55
+ mkdirSync(dirname(paths.settings), { recursive: true });
56
+ writeFileSync(paths.settings, JSON.stringify(settings, null, 2), { mode: 0o600 });
57
+ return settingsStatus();
58
+ }
59
+
30
60
  export function saveClipboardEnabled(enabled) {
31
61
  const settings = readSettings();
32
62
  settings.clipboardEnabled = Boolean(enabled);
@@ -0,0 +1,29 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { paths } from '../config.js';
4
+
5
+ function normalize(item, index) {
6
+ const id = item.id || `static-${index + 1}`;
7
+ return {
8
+ id,
9
+ name: item.name || `Page ${index + 1}`,
10
+ url: item.url || '',
11
+ icon: item.icon || 'GlobalOutlined'
12
+ };
13
+ }
14
+
15
+ export function listStaticPages() {
16
+ if (!existsSync(paths.staticPages)) return [];
17
+ try {
18
+ const raw = JSON.parse(readFileSync(paths.staticPages, 'utf8'));
19
+ if (!Array.isArray(raw)) return [];
20
+ return raw.map(normalize);
21
+ } catch {
22
+ return [];
23
+ }
24
+ }
25
+
26
+ export function saveStaticPages(pages) {
27
+ mkdirSync(dirname(paths.staticPages), { recursive: true });
28
+ writeFileSync(paths.staticPages, JSON.stringify(pages, null, 2));
29
+ }
@@ -0,0 +1,16 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { paths } from '../config.js';
4
+
5
+ export function listTodos() {
6
+ try {
7
+ return existsSync(paths.todos) ? JSON.parse(readFileSync(paths.todos, 'utf8')) : [];
8
+ } catch {
9
+ return [];
10
+ }
11
+ }
12
+
13
+ export function saveTodos(todos) {
14
+ mkdirSync(dirname(paths.todos), { recursive: true });
15
+ writeFileSync(paths.todos, JSON.stringify(todos, null, 2));
16
+ }
@@ -1,8 +1,9 @@
1
1
  import express, { Router } from 'express';
2
- import { clipboardDates, clipboardItems, clipboardOriginal, deleteClipboardItem, saveClipboardImage, clipboardImagePath } from '../services/clipboard-history.js';
2
+ import { clipboardDates, clipboardItems, clipboardOriginal, deleteClipboardItem, saveClipboardImage, clipboardImagePath, allTaggedClipboardItems, updateClipboardItemTags } from '../services/clipboard-history.js';
3
3
 
4
4
  const router = Router();
5
5
  router.get('/', (req, res) => res.json({ dates: clipboardDates(), items: clipboardItems(req.query.date) }));
6
+ router.get('/tagged', (_req, res) => res.json({ items: allTaggedClipboardItems() }));
6
7
 
7
8
  router.post('/image', express.raw({ type: 'image/*', limit: '10mb' }), (req, res) => {
8
9
  try {
@@ -10,7 +11,9 @@ router.post('/image', express.raw({ type: 'image/*', limit: '10mb' }), (req, res
10
11
  if (!buffer || buffer.length === 0) {
11
12
  return res.status(400).json({ error: 'Image body is empty.' });
12
13
  }
13
- const item = saveClipboardImage(buffer);
14
+ const contentType = req.headers['content-type'] || 'image/png';
15
+ const ext = contentType.includes('jpeg') || contentType.includes('jpg') ? 'jpg' : 'png';
16
+ const item = saveClipboardImage(buffer, ext);
14
17
  res.json(item);
15
18
  } catch (error) {
16
19
  res.status(500).json({ error: error.message });
@@ -27,5 +30,11 @@ router.get('/image/:filename', (req, res) => {
27
30
  });
28
31
 
29
32
  router.get('/:date/:id', (req, res) => { const text = clipboardOriginal(req.params.date, req.params.id); if (text === null) return res.status(404).json({ error: 'Clipboard entry not found.' }); res.type('text/plain').send(text); });
33
+ router.put('/:date/:id/tags', (req, res) => {
34
+ const { tags } = req.body || {};
35
+ const updated = updateClipboardItemTags(req.params.date, req.params.id, tags);
36
+ if (!updated) return res.status(404).json({ error: 'Clipboard entry not found.' });
37
+ res.json(updated);
38
+ });
30
39
  router.delete('/:date/:id', (req, res) => { if (!deleteClipboardItem(req.params.date, req.params.id)) return res.status(404).json({ error: 'Clipboard entry not found.' }); res.status(204).end(); });
31
40
  export default router;
@@ -0,0 +1,160 @@
1
+ import https from 'node:https';
2
+ import axios from 'axios';
3
+ import { Router } from 'express';
4
+ import { readSettings } from '../repositories/settings.js';
5
+ import { listJiraFilters, saveJiraFilters } from '../repositories/jira-filters.js';
6
+
7
+ const router = Router();
8
+ const httpsAgent = new https.Agent({ rejectUnauthorized: false });
9
+ const httpClient = axios.create({
10
+ httpsAgent,
11
+ validateStatus: () => true
12
+ });
13
+
14
+ function getJiraHost() {
15
+ const settings = readSettings();
16
+ const domain = settings.domain || '';
17
+ if (!domain) return '';
18
+ const clean = domain.replace(/^https?:\/\//i, '').replace(/\/+$/, '');
19
+ if (clean.includes('jira') || clean.includes('.atlassian.net')) return clean;
20
+ return `jira.${clean}`;
21
+ }
22
+
23
+ function getPriorityRank(priority) {
24
+ if (!priority) return 0;
25
+ const name = (typeof priority === 'string' ? priority : priority.name || '').toLowerCase();
26
+ if (name.includes('blocker') || name.includes('highest') || name.includes('p0') || name.includes('urgent')) return 50;
27
+ if (name.includes('critical') || name.includes('high') || name.includes('p1')) return 40;
28
+ if (name.includes('major') || name.includes('medium') || name.includes('normal') || name.includes('p2')) return 30;
29
+ if (name.includes('minor') || name.includes('low') || name.includes('p3')) return 20;
30
+ if (name.includes('trivial') || name.includes('lowest') || name.includes('p4')) return 10;
31
+
32
+ if (typeof priority === 'object' && priority.id && !isNaN(Number(priority.id))) {
33
+ return 100 - Number(priority.id);
34
+ }
35
+ return 0;
36
+ }
37
+
38
+ // List all filters
39
+ router.get('/', (_req, res) => {
40
+ res.json(listJiraFilters());
41
+ });
42
+
43
+ // Create filter
44
+ router.post('/', (req, res) => {
45
+ const { name, filterId } = req.body || {};
46
+ if (!name || typeof name !== 'string' || !name.trim()) {
47
+ return res.status(400).json({ error: 'Filter name is required.' });
48
+ }
49
+ if (!filterId || (typeof filterId !== 'string' && typeof filterId !== 'number') || !String(filterId).trim()) {
50
+ return res.status(400).json({ error: 'Filter ID is required.' });
51
+ }
52
+
53
+ const filters = listJiraFilters();
54
+ const newFilter = {
55
+ id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
56
+ name: name.trim(),
57
+ filterId: String(filterId).trim(),
58
+ createdAt: new Date().toISOString()
59
+ };
60
+ filters.push(newFilter);
61
+ saveJiraFilters(filters);
62
+ res.status(201).json(newFilter);
63
+ });
64
+
65
+ // Update filter
66
+ router.put('/:id', (req, res) => {
67
+ const { id } = req.params;
68
+ const { name, filterId } = req.body || {};
69
+ const filters = listJiraFilters();
70
+ const index = filters.findIndex((f) => f.id === id);
71
+ if (index === -1) {
72
+ return res.status(404).json({ error: 'Filter not found.' });
73
+ }
74
+
75
+ if (name !== undefined) {
76
+ if (typeof name !== 'string' || !name.trim()) {
77
+ return res.status(400).json({ error: 'Filter name cannot be empty.' });
78
+ }
79
+ filters[index].name = name.trim();
80
+ }
81
+
82
+ if (filterId !== undefined) {
83
+ if ((typeof filterId !== 'string' && typeof filterId !== 'number') || !String(filterId).trim()) {
84
+ return res.status(400).json({ error: 'Filter ID cannot be empty.' });
85
+ }
86
+ filters[index].filterId = String(filterId).trim();
87
+ }
88
+
89
+ filters[index].updatedAt = new Date().toISOString();
90
+ saveJiraFilters(filters);
91
+ res.json(filters[index]);
92
+ });
93
+
94
+ // Delete filter
95
+ router.delete('/:id', (req, res) => {
96
+ const { id } = req.params;
97
+ const filters = listJiraFilters();
98
+ const filtered = filters.filter((f) => f.id !== id);
99
+ if (filtered.length === filters.length) {
100
+ return res.status(404).json({ error: 'Filter not found.' });
101
+ }
102
+ saveJiraFilters(filtered);
103
+ res.status(204).end();
104
+ });
105
+
106
+ // Fetch issues for a filter
107
+ router.get('/:id/issues', async (req, res) => {
108
+ const { id } = req.params;
109
+ const filters = listJiraFilters();
110
+ const filter = filters.find((f) => f.id === id);
111
+ if (!filter) {
112
+ return res.status(404).json({ error: 'Filter not found.' });
113
+ }
114
+
115
+ const jiraHost = getJiraHost();
116
+ if (!jiraHost) {
117
+ return res.status(400).json({ error: 'Jira domain is not configured. Please set Domain in Settings.' });
118
+ }
119
+
120
+ const settings = readSettings();
121
+ const token = settings.jiraAccessToken;
122
+
123
+ try {
124
+ const url = `https://${jiraHost}/rest/api/2/search?jql=filter%3D${encodeURIComponent(filter.filterId)}&maxResults=200&fields=summary,priority,duedate,status`;
125
+ const headers = {};
126
+ if (token) {
127
+ headers.Authorization = token.startsWith('Bearer ') ? token : `Bearer ${token}`;
128
+ }
129
+
130
+ const response = await httpClient.get(url, { headers });
131
+ if (response.status !== 200) {
132
+ const errMsg = response.data?.errorMessages?.[0] || response.data?.message || `Jira API returned status ${response.status}`;
133
+ return res.status(response.status).json({ error: errMsg });
134
+ }
135
+
136
+ const rawIssues = response.data?.issues || [];
137
+ const issues = rawIssues.map((issue) => {
138
+ const key = issue.key || issue.id;
139
+ const fields = issue.fields || {};
140
+ return {
141
+ id: issue.id,
142
+ key,
143
+ summary: fields.summary || '(No summary)',
144
+ priority: fields.priority || null,
145
+ dueDate: fields.duedate || null,
146
+ status: fields.status?.name || null,
147
+ url: `https://${jiraHost}/browse/${key}`
148
+ };
149
+ });
150
+
151
+ // Sort by priority descending
152
+ issues.sort((a, b) => getPriorityRank(b.priority) - getPriorityRank(a.priority));
153
+
154
+ res.json({ filter, issues });
155
+ } catch (error) {
156
+ res.status(500).json({ error: error.message || 'Failed to fetch Jira issues.' });
157
+ }
158
+ });
159
+
160
+ export default router;
@@ -1,6 +1,6 @@
1
1
  import { Router } from 'express';
2
2
  import { listLaunchers, saveLaunchers } from '../repositories/launchers.js';
3
- import { runScript, runningScripts, scriptErrorLogs, scriptLogs, stopScript } from '../services/process-manager.js';
3
+ import { runScript, runningErrorCounts, runningScripts, scriptErrorLogs, scriptLogs, stopScript } from '../services/process-manager.js';
4
4
  import { readPackageScripts } from '../services/package-scripts.js';
5
5
  import { currentGitBranch } from '../services/git.js';
6
6
 
@@ -23,7 +23,7 @@ router.get('/', async (_req, res) => {
23
23
  router.post('/', (req, res) => { const config = validLauncher(req.body); if (!config) return invalid(res); const launcher = { id: crypto.randomUUID(), ...config }; const launchers = listLaunchers(); saveLaunchers([...launchers, launcher]); res.status(201).json(launcher); });
24
24
  router.put('/:id', (req, res) => { const config = validLauncher(req.body); if (!config) return invalid(res); const launchers = listLaunchers(); const index = launchers.findIndex((item) => item.id === req.params.id); if (index === -1) return res.status(404).json({ error: 'Configuration not found.' }); const launcher = { id: req.params.id, ...config }; launchers[index] = launcher; saveLaunchers(launchers); res.json(launcher); });
25
25
  router.delete('/:id', (req, res) => { const launchers = listLaunchers(); const next = launchers.filter((item) => item.id !== req.params.id); if (next.length === launchers.length) return res.status(404).json({ error: 'Configuration not found.' }); saveLaunchers(next); res.status(204).end(); });
26
- router.get('/running', (_req, res) => res.json(runningScripts()));
26
+ router.get('/running', (_req, res) => res.json({ running: runningScripts(), errorCounts: runningErrorCounts() }));
27
27
  router.post('/:id/stop', (req, res) => { try { stopScript(req.params.id, req.body?.scriptId); res.json({ ok: true }); } catch (error) { res.status(404).json({ error: error.message }); } });
28
28
  router.post('/:id/start', (req, res) => {
29
29
  const launcher = listLaunchers().find((item) => item.id === req.params.id);
@@ -1,3 +1,5 @@
1
+ import https from 'node:https';
2
+ import axios from 'axios';
1
3
  import { Router } from 'express';
2
4
  import { readSettings } from '../repositories/settings.js';
3
5
 
@@ -5,6 +7,12 @@ const router = Router();
5
7
  const previewLimit = 2000;
6
8
  let lastUsedHost = '';
7
9
 
10
+ const httpsAgent = new https.Agent({ rejectUnauthorized: false });
11
+ const httpClient = axios.create({
12
+ httpsAgent,
13
+ validateStatus: () => true
14
+ });
15
+
8
16
  function getBitbucketHost() {
9
17
  if (lastUsedHost) return lastUsedHost;
10
18
  const settings = readSettings();
@@ -202,22 +210,22 @@ router.post('/check', async (req, res) => {
202
210
  try {
203
211
  // 1. Fetch Pull Request details to show basic info
204
212
  const prUrl = `https://${host}/rest/api/1.0/projects/${projectKey}/repos/${repositorySlug}/pull-requests/${pullRequestId}`;
205
- const prRes = await fetch(prUrl, { headers });
206
- if (!prRes.ok) {
213
+ const prRes = await httpClient.get(prUrl, { headers });
214
+ if (prRes.status < 200 || prRes.status >= 300) {
207
215
  if (prRes.status === 401) {
208
216
  return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Access Token in Settings.' });
209
217
  }
210
- return res.status(prRes.status).json({ error: `Failed to fetch PR info: ${prRes.statusText}` });
218
+ return res.status(prRes.status).json({ error: `Failed to fetch PR info: ${prRes.statusText || prRes.status}` });
211
219
  }
212
- const prInfo = await prRes.json();
220
+ const prInfo = prRes.data;
213
221
 
214
222
  // 2. Fetch changes
215
223
  const changesUrl = `https://${host}/rest/api/1.0/projects/${projectKey}/repos/${repositorySlug}/pull-requests/${pullRequestId}/changes?limit=1000`;
216
- const changesRes = await fetch(changesUrl, { headers });
217
- if (!changesRes.ok) {
218
- return res.status(changesRes.status).json({ error: `Failed to fetch PR changes: ${changesRes.statusText}` });
224
+ const changesRes = await httpClient.get(changesUrl, { headers });
225
+ if (changesRes.status < 200 || changesRes.status >= 300) {
226
+ return res.status(changesRes.status).json({ error: `Failed to fetch PR changes: ${changesRes.statusText || changesRes.status}` });
219
227
  }
220
- const changesData = await changesRes.json();
228
+ const changesData = changesRes.data;
221
229
 
222
230
  const allChanges = changesData.values || [];
223
231
  const filteredOut = [];
@@ -242,10 +250,10 @@ router.post('/check', async (req, res) => {
242
250
  for (const change of targetChanges) {
243
251
  const filePath = change.path.toString;
244
252
  const diffUrl = `https://${host}/rest/api/1.0/projects/${projectKey}/repos/${repositorySlug}/pull-requests/${pullRequestId}/diff/${encodeURIComponent(filePath)}?context=0`;
245
- const diffRes = await fetch(diffUrl, { headers });
246
- if (!diffRes.ok) continue; // skip file if diff cannot be retrieved
253
+ const diffRes = await httpClient.get(diffUrl, { headers });
254
+ if (diffRes.status < 200 || diffRes.status >= 300) continue; // skip file if diff cannot be retrieved
247
255
 
248
- const diffData = await diffRes.json();
256
+ const diffData = diffRes.data;
249
257
  const fileIssues = analyzeDiff(filePath, diffData.hunks);
250
258
 
251
259
  if (fileIssues.length > 0) {
@@ -323,14 +331,14 @@ router.get('/my-prs', async (req, res) => {
323
331
 
324
332
  try {
325
333
  const url = `https://${host}/rest/api/1.0/dashboard/pull-requests?role=author&state=OPEN&limit=100`;
326
- const response = await fetch(url, { headers });
327
- if (!response.ok) {
334
+ const response = await httpClient.get(url, { headers });
335
+ if (response.status < 200 || response.status >= 300) {
328
336
  if (response.status === 401) {
329
337
  return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Token.' });
330
338
  }
331
- return res.status(response.status).json({ error: `Bitbucket API error: ${response.statusText}` });
339
+ return res.status(response.status).json({ error: `Bitbucket API error: ${response.statusText || response.status}` });
332
340
  }
333
- const data = await response.json();
341
+ const data = response.data;
334
342
  const values = data.values || [];
335
343
 
336
344
  const sorted = values.sort((a, b) => {
@@ -357,14 +365,14 @@ router.get('/review-prs', async (req, res) => {
357
365
 
358
366
  try {
359
367
  const url = `https://${host}/rest/api/1.0/dashboard/pull-requests?role=reviewer&state=OPEN&limit=100`;
360
- const response = await fetch(url, { headers });
361
- if (!response.ok) {
368
+ const response = await httpClient.get(url, { headers });
369
+ if (response.status < 200 || response.status >= 300) {
362
370
  if (response.status === 401) {
363
371
  return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Token.' });
364
372
  }
365
- return res.status(response.status).json({ error: `Bitbucket API error: ${response.statusText}` });
373
+ return res.status(response.status).json({ error: `Bitbucket API error: ${response.statusText || response.status}` });
366
374
  }
367
- const data = await response.json();
375
+ const data = response.data;
368
376
  const values = data.values || [];
369
377
 
370
378
  const sorted = values.sort((a, b) => (b.updatedDate || 0) - (a.updatedDate || 0));
@@ -410,20 +418,16 @@ router.post('/comment', async (req, res) => {
410
418
  }
411
419
 
412
420
  try {
413
- const response = await fetch(url, {
414
- method: 'POST',
415
- headers,
416
- body: JSON.stringify(payload)
417
- });
421
+ const response = await httpClient.post(url, payload, { headers });
418
422
 
419
- if (!response.ok) {
420
- const errJson = await response.json().catch(() => ({}));
423
+ if (response.status < 200 || response.status >= 300) {
424
+ const errJson = response.data || {};
421
425
  return res.status(response.status).json({
422
- error: errJson.errors?.[0]?.message || errJson.message || `Bitbucket API error: ${response.statusText}`
426
+ error: errJson.errors?.[0]?.message || errJson.message || `Bitbucket API error: ${response.statusText || response.status}`
423
427
  });
424
428
  }
425
429
 
426
- const data = await response.json();
430
+ const data = response.data;
427
431
  res.json({ success: true, data });
428
432
  } catch (error) {
429
433
  res.status(500).json({ error: error.message });
@@ -1,5 +1,6 @@
1
1
  import { Router } from 'express';
2
- import { saveAccessToken, saveClipboardEnabled, saveDomain, settingsStatus } from '../repositories/settings.js';
2
+ import { saveAccessToken, saveClipboardEnabled, saveDefaultBrowser, saveDefaultEditor, saveDomain, saveJiraIssuePrefix, settingsStatus } from '../repositories/settings.js';
3
+ import { openInSystemBrowser } from '../services/browser.js';
3
4
 
4
5
  const router = Router();
5
6
 
@@ -9,6 +10,27 @@ router.put('/domain', (req, res) => {
9
10
  if (typeof domain !== 'string') return res.status(400).json({ error: 'Domain must be a string.' });
10
11
  res.json(saveDomain(domain.trim()));
11
12
  });
13
+ router.put('/jira-issue-prefix', (req, res) => {
14
+ const { jiraIssuePrefix } = req.body || {};
15
+ if (typeof jiraIssuePrefix !== 'string') return res.status(400).json({ error: 'Jira issue prefix must be a string.' });
16
+ res.json(saveJiraIssuePrefix(jiraIssuePrefix.trim()));
17
+ });
18
+ router.put('/default-editor', (req, res) => {
19
+ const { defaultEditor } = req.body || {};
20
+ if (typeof defaultEditor !== 'string') return res.status(400).json({ error: 'defaultEditor must be a string.' });
21
+ res.json(saveDefaultEditor(defaultEditor.trim()));
22
+ });
23
+ router.put('/default-browser', (req, res) => {
24
+ const { defaultBrowser } = req.body || {};
25
+ if (typeof defaultBrowser !== 'string') return res.status(400).json({ error: 'defaultBrowser must be a string.' });
26
+ res.json(saveDefaultBrowser(defaultBrowser.trim()));
27
+ });
28
+ router.post('/open-url', (req, res) => {
29
+ const { url } = req.body || {};
30
+ if (typeof url !== 'string' || !url) return res.status(400).json({ error: 'URL must be a non-empty string.' });
31
+ openInSystemBrowser(url.trim());
32
+ res.json({ success: true });
33
+ });
12
34
  router.put('/clipboard-enabled', (req, res) => {
13
35
  const { enabled } = req.body || {};
14
36
  if (typeof enabled !== 'boolean') return res.status(400).json({ error: 'Enabled must be a boolean.' });
@@ -0,0 +1,20 @@
1
+ import { Router } from 'express';
2
+ import { listStaticPages, saveStaticPages } from '../repositories/static-pages.js';
3
+
4
+ const router = Router();
5
+
6
+ // List static pages
7
+ router.get('/', (_req, res) => {
8
+ res.json(listStaticPages());
9
+ });
10
+
11
+ // Update static pages
12
+ router.post('/', (req, res) => {
13
+ if (!Array.isArray(req.body)) {
14
+ return res.status(400).json({ error: 'Expected an array of static pages.' });
15
+ }
16
+ saveStaticPages(req.body);
17
+ res.json(listStaticPages());
18
+ });
19
+
20
+ export default router;