buddy-workbench 0.1.12 → 0.1.14

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.12",
3
+ "version": "0.1.14",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
package/server/config.js CHANGED
@@ -11,6 +11,7 @@ export const paths = {
11
11
  todos: join(root, 'data', 'todos.json'),
12
12
  staticPages: join(root, 'data', 'static-pages.json'),
13
13
  errors: join(root, 'data', 'errors.json'),
14
+ postman: join(root, 'data', 'postman.json'),
14
15
  shutdownLog: join(root, 'data', 'shutdown.log'),
15
16
  clipboardDir: join(root, 'data', 'clipboard'),
16
17
  plugins: join(root, 'plugins'),
@@ -0,0 +1,31 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { paths } from '../config.js';
4
+
5
+ const defaultData = {
6
+ collections: [],
7
+ environments: [],
8
+ activeEnvironmentId: null
9
+ };
10
+
11
+ export function getPostmanData() {
12
+ try {
13
+ if (!existsSync(paths.postman)) {
14
+ return defaultData;
15
+ }
16
+ const raw = readFileSync(paths.postman, 'utf8');
17
+ const parsed = JSON.parse(raw);
18
+ return {
19
+ collections: parsed.collections || [],
20
+ environments: parsed.environments || [],
21
+ activeEnvironmentId: parsed.activeEnvironmentId || null
22
+ };
23
+ } catch (error) {
24
+ return defaultData;
25
+ }
26
+ }
27
+
28
+ export function savePostmanData(data) {
29
+ mkdirSync(dirname(paths.postman), { recursive: true });
30
+ writeFileSync(paths.postman, JSON.stringify(data, null, 2), 'utf8');
31
+ }
@@ -17,7 +17,10 @@ export function settingsStatus() {
17
17
  bitbucketTokenConfigured: Boolean(settings.bitbucketAccessToken),
18
18
  jiraTokenConfigured: Boolean(settings.jiraAccessToken),
19
19
  confluenceTokenConfigured: Boolean(settings.confluenceAccessToken),
20
- clipboardEnabled: settings.clipboardEnabled !== false
20
+ isMac: process.platform === 'darwin',
21
+ clipboardEnabled: settings.clipboardEnabled !== false,
22
+ clipboardImageEnabled: settings.clipboardImageEnabled !== false,
23
+ clipboardDeduplicateMinutes: typeof settings.clipboardDeduplicateMinutes === 'number' && !isNaN(settings.clipboardDeduplicateMinutes) ? settings.clipboardDeduplicateMinutes : 60
21
24
  };
22
25
  }
23
26
 
@@ -65,6 +68,23 @@ export function saveClipboardEnabled(enabled) {
65
68
  return settingsStatus();
66
69
  }
67
70
 
71
+ export function saveClipboardImageEnabled(enabled) {
72
+ const settings = readSettings();
73
+ settings.clipboardImageEnabled = Boolean(enabled);
74
+ mkdirSync(dirname(paths.settings), { recursive: true });
75
+ writeFileSync(paths.settings, JSON.stringify(settings, null, 2), { mode: 0o600 });
76
+ return settingsStatus();
77
+ }
78
+
79
+ export function saveClipboardDeduplicateMinutes(minutes) {
80
+ const settings = readSettings();
81
+ const val = Number(minutes);
82
+ settings.clipboardDeduplicateMinutes = !isNaN(val) && val >= 0 ? val : 60;
83
+ mkdirSync(dirname(paths.settings), { recursive: true });
84
+ writeFileSync(paths.settings, JSON.stringify(settings, null, 2), { mode: 0o600 });
85
+ return settingsStatus();
86
+ }
87
+
68
88
  const accessTokenFields = {
69
89
  bitbucket: 'bitbucketAccessToken',
70
90
  jira: 'jiraAccessToken',
@@ -0,0 +1,42 @@
1
+ import { Router } from 'express';
2
+ import {
3
+ getBookmarkSyncStatus,
4
+ previewSync,
5
+ performSync
6
+ } from '../services/bookmark-sync.js';
7
+
8
+ const router = Router();
9
+
10
+ // Get status of Chrome & Edge bookmark files & processes
11
+ router.get('/status', (req, res, next) => {
12
+ try {
13
+ const status = getBookmarkSyncStatus();
14
+ res.json(status);
15
+ } catch (error) {
16
+ next(error);
17
+ }
18
+ });
19
+
20
+ // Preview sync changes
21
+ router.get('/preview', (req, res, next) => {
22
+ try {
23
+ const mode = req.query.mode || 'two-way';
24
+ const preview = previewSync(mode);
25
+ res.json(preview);
26
+ } catch (error) {
27
+ next(error);
28
+ }
29
+ });
30
+
31
+ // Perform actual sync
32
+ router.post('/sync', (req, res, next) => {
33
+ try {
34
+ const mode = req.body?.mode || 'two-way';
35
+ const result = performSync(mode);
36
+ res.json(result);
37
+ } catch (error) {
38
+ next(error);
39
+ }
40
+ });
41
+
42
+ export default router;
@@ -0,0 +1,283 @@
1
+ import { Router } from 'express';
2
+ import axios from 'axios';
3
+ import { getPostmanData, savePostmanData } from '../repositories/postman.js';
4
+ import { parsePostmanCollection } from '../services/postman-parser.js';
5
+
6
+ const router = Router();
7
+
8
+ // Variable substitution helper
9
+ function replaceVariables(str, varMap) {
10
+ if (typeof str !== 'string') return str;
11
+ return str.replace(/\{\{\s*([\w.-]+)\s*\}\}/g, (match, key) => {
12
+ return varMap[key] !== undefined ? varMap[key] : match;
13
+ });
14
+ }
15
+
16
+ // GET all Postman workbench data
17
+ router.get('/', (req, res) => {
18
+ try {
19
+ const data = getPostmanData();
20
+ res.json(data);
21
+ } catch (error) {
22
+ res.status(500).json({ error: error.message || 'Failed to fetch Postman data' });
23
+ }
24
+ });
25
+
26
+ // Import collection
27
+ router.post('/import', (req, res) => {
28
+ try {
29
+ const { collectionJson } = req.body;
30
+ if (!collectionJson) {
31
+ return res.status(400).json({ error: 'Collection JSON content is required' });
32
+ }
33
+
34
+ const parsedCollection = parsePostmanCollection(collectionJson);
35
+ const data = getPostmanData();
36
+
37
+ // Check if collection with same ID or name exists
38
+ const existingIndex = data.collections.findIndex((c) => c.id === parsedCollection.id);
39
+ if (existingIndex >= 0) {
40
+ data.collections[existingIndex] = parsedCollection;
41
+ } else {
42
+ data.collections.unshift(parsedCollection);
43
+ }
44
+
45
+ savePostmanData(data);
46
+ res.status(201).json(parsedCollection);
47
+ } catch (error) {
48
+ res.status(400).json({ error: `Invalid Postman collection JSON: ${error.message}` });
49
+ }
50
+ });
51
+
52
+ // Create collection manually
53
+ router.post('/collections', (req, res) => {
54
+ try {
55
+ const { name, description } = req.body;
56
+ if (!name) {
57
+ return res.status(400).json({ error: 'Collection name is required' });
58
+ }
59
+
60
+ const data = getPostmanData();
61
+ const newCollection = {
62
+ id: `col_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
63
+ name,
64
+ description: description || '',
65
+ variables: [],
66
+ items: [],
67
+ createdAt: new Date().toISOString()
68
+ };
69
+
70
+ data.collections.unshift(newCollection);
71
+ savePostmanData(data);
72
+ res.status(201).json(newCollection);
73
+ } catch (error) {
74
+ res.status(500).json({ error: error.message || 'Failed to create collection' });
75
+ }
76
+ });
77
+
78
+ // Update collection
79
+ router.put('/collections/:id', (req, res) => {
80
+ try {
81
+ const { id } = req.params;
82
+ const { name, description, variables, items } = req.body;
83
+
84
+ const data = getPostmanData();
85
+ const index = data.collections.findIndex((c) => c.id === id);
86
+ if (index === -1) {
87
+ return res.status(404).json({ error: 'Collection not found' });
88
+ }
89
+
90
+ const updated = {
91
+ ...data.collections[index],
92
+ ...(name !== undefined && { name }),
93
+ ...(description !== undefined && { description }),
94
+ ...(variables !== undefined && { variables }),
95
+ ...(items !== undefined && { items }),
96
+ updatedAt: new Date().toISOString()
97
+ };
98
+
99
+ data.collections[index] = updated;
100
+ savePostmanData(data);
101
+ res.json(updated);
102
+ } catch (error) {
103
+ res.status(500).json({ error: error.message || 'Failed to update collection' });
104
+ }
105
+ });
106
+
107
+ // Delete collection
108
+ router.delete('/collections/:id', (req, res) => {
109
+ try {
110
+ const { id } = req.params;
111
+ const data = getPostmanData();
112
+ data.collections = data.collections.filter((c) => c.id !== id);
113
+ savePostmanData(data);
114
+ res.json({ success: true });
115
+ } catch (error) {
116
+ res.status(500).json({ error: error.message || 'Failed to delete collection' });
117
+ }
118
+ });
119
+
120
+ // Save environments
121
+ router.post('/environments', (req, res) => {
122
+ try {
123
+ const { environments, activeEnvironmentId } = req.body;
124
+ const data = getPostmanData();
125
+
126
+ if (Array.isArray(environments)) {
127
+ data.environments = environments;
128
+ }
129
+ if (activeEnvironmentId !== undefined) {
130
+ data.activeEnvironmentId = activeEnvironmentId;
131
+ }
132
+
133
+ savePostmanData(data);
134
+ res.json({
135
+ environments: data.environments,
136
+ activeEnvironmentId: data.activeEnvironmentId
137
+ });
138
+ } catch (error) {
139
+ res.status(500).json({ error: error.message || 'Failed to save environments' });
140
+ }
141
+ });
142
+
143
+ // HTTP Request Proxy execution
144
+ router.post('/send', async (req, res) => {
145
+ try {
146
+ const {
147
+ method = 'GET',
148
+ url = '',
149
+ params = [],
150
+ headers = [],
151
+ body = {},
152
+ auth = {},
153
+ environmentVariables = [],
154
+ collectionVariables = []
155
+ } = req.body;
156
+
157
+ if (!url) {
158
+ return res.status(400).json({ error: 'Target URL is required' });
159
+ }
160
+
161
+ // Build variable mapping (environment overrides collection)
162
+ const varMap = {};
163
+ if (Array.isArray(collectionVariables)) {
164
+ collectionVariables.forEach((v) => {
165
+ if (v.enabled !== false && v.key) varMap[v.key] = v.value || '';
166
+ });
167
+ }
168
+ if (Array.isArray(environmentVariables)) {
169
+ environmentVariables.forEach((v) => {
170
+ if (v.enabled !== false && v.key) varMap[v.key] = v.value || '';
171
+ });
172
+ }
173
+
174
+ // Replace variables in URL
175
+ let resolvedUrl = replaceVariables(url, varMap);
176
+ if (!resolvedUrl.startsWith('http://') && !resolvedUrl.startsWith('https://')) {
177
+ resolvedUrl = `http://${resolvedUrl}`;
178
+ }
179
+
180
+ // Process Params
181
+ const queryObj = {};
182
+ if (Array.isArray(params)) {
183
+ params.forEach((p) => {
184
+ if (p.enabled !== false && p.key) {
185
+ queryObj[replaceVariables(p.key, varMap)] = replaceVariables(p.value || '', varMap);
186
+ }
187
+ });
188
+ }
189
+
190
+ // Process Headers
191
+ const reqHeaders = {};
192
+ if (Array.isArray(headers)) {
193
+ headers.forEach((h) => {
194
+ if (h.enabled !== false && h.key) {
195
+ reqHeaders[replaceVariables(h.key, varMap)] = replaceVariables(h.value || '', varMap);
196
+ }
197
+ });
198
+ }
199
+
200
+ // Process Auth
201
+ if (auth && auth.type === 'bearer' && auth.token) {
202
+ reqHeaders['Authorization'] = `Bearer ${replaceVariables(auth.token, varMap)}`;
203
+ } else if (auth && auth.type === 'basic' && (auth.username || auth.password)) {
204
+ const u = replaceVariables(auth.username || '', varMap);
205
+ const p = replaceVariables(auth.password || '', varMap);
206
+ const credentials = Buffer.from(`${u}:${p}`).toString('base64');
207
+ reqHeaders['Authorization'] = `Basic ${credentials}`;
208
+ }
209
+
210
+ // Process Body
211
+ let reqData = undefined;
212
+ if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method.toUpperCase())) {
213
+ if (body.mode === 'raw' || body.mode === 'json') {
214
+ const rawContent = replaceVariables(body.raw || body.json || '', varMap);
215
+ if (rawContent) {
216
+ reqData = rawContent;
217
+ if (!Object.keys(reqHeaders).some((k) => k.toLowerCase() === 'content-type')) {
218
+ reqHeaders['Content-Type'] = 'application/json';
219
+ }
220
+ }
221
+ } else if (body.mode === 'urlencoded') {
222
+ const urlParams = new URLSearchParams();
223
+ if (Array.isArray(body.urlencoded)) {
224
+ body.urlencoded.forEach((u) => {
225
+ if (u.enabled !== false && u.key) {
226
+ urlParams.append(replaceVariables(u.key, varMap), replaceVariables(u.value || '', varMap));
227
+ }
228
+ });
229
+ }
230
+ reqData = urlParams.toString();
231
+ if (!Object.keys(reqHeaders).some((k) => k.toLowerCase() === 'content-type')) {
232
+ reqHeaders['Content-Type'] = 'application/x-www-form-urlencoded';
233
+ }
234
+ }
235
+ }
236
+
237
+ const startTime = Date.now();
238
+ try {
239
+ const response = await axios({
240
+ method,
241
+ url: resolvedUrl,
242
+ params: Object.keys(queryObj).length > 0 ? queryObj : undefined,
243
+ headers: reqHeaders,
244
+ data: reqData,
245
+ validateStatus: () => true, // Don't throw on non-2xx status codes
246
+ timeout: 30000,
247
+ maxContentLength: 10 * 1024 * 1024 // 10MB limit
248
+ });
249
+
250
+ const durationMs = Date.now() - startTime;
251
+ let responseBody = response.data;
252
+ const responseHeaders = response.headers || {};
253
+ const contentType = responseHeaders['content-type'] || responseHeaders['Content-Type'] || '';
254
+
255
+ const bodyString = typeof responseBody === 'string'
256
+ ? responseBody
257
+ : JSON.stringify(responseBody || '');
258
+ const sizeBytes = Buffer.byteLength(bodyString, 'utf8');
259
+
260
+ res.json({
261
+ status: response.status,
262
+ statusText: response.statusText,
263
+ headers: responseHeaders,
264
+ data: responseBody,
265
+ durationMs,
266
+ sizeBytes,
267
+ contentType,
268
+ resolvedUrl
269
+ });
270
+ } catch (reqErr) {
271
+ const durationMs = Date.now() - startTime;
272
+ res.status(502).json({
273
+ error: reqErr.message || 'Request execution failed',
274
+ resolvedUrl,
275
+ durationMs
276
+ });
277
+ }
278
+ } catch (error) {
279
+ res.status(500).json({ error: error.message || 'Failed to send request' });
280
+ }
281
+ });
282
+
283
+ export default router;
@@ -1,6 +1,7 @@
1
1
  import { Router } from 'express';
2
- import { saveAccessToken, saveClipboardEnabled, saveDefaultBrowser, saveDefaultEditor, saveDomain, saveJiraIssuePrefix, settingsStatus } from '../repositories/settings.js';
3
- import { openInSystemBrowser } from '../services/browser.js';
2
+ import { saveAccessToken, saveClipboardDeduplicateMinutes, saveClipboardEnabled, saveClipboardImageEnabled, saveDefaultBrowser, saveDefaultEditor, saveDomain, saveJiraIssuePrefix, settingsStatus } from '../repositories/settings.js';
3
+ import { openInSystemBrowser, openInSystemEditor } from '../services/browser.js';
4
+ import { selectDirectory } from '../services/dialog.js';
4
5
 
5
6
  const router = Router();
6
7
 
@@ -31,11 +32,35 @@ router.post('/open-url', (req, res) => {
31
32
  openInSystemBrowser(url.trim());
32
33
  res.json({ success: true });
33
34
  });
35
+ router.post('/open-editor', (req, res) => {
36
+ const { path, location } = req.body || {};
37
+ if (typeof path !== 'string' || !path) return res.status(400).json({ error: 'Path must be a non-empty string.' });
38
+ openInSystemEditor(path.trim(), location ? String(location).trim() : '');
39
+ res.json({ success: true });
40
+ });
41
+ router.post('/select-directory', async (_req, res) => {
42
+ try {
43
+ const result = await selectDirectory();
44
+ res.json(result);
45
+ } catch (error) {
46
+ res.status(500).json({ error: error.message });
47
+ }
48
+ });
34
49
  router.put('/clipboard-enabled', (req, res) => {
35
50
  const { enabled } = req.body || {};
36
51
  if (typeof enabled !== 'boolean') return res.status(400).json({ error: 'Enabled must be a boolean.' });
37
52
  res.json(saveClipboardEnabled(enabled));
38
53
  });
54
+ router.put('/clipboard-image-enabled', (req, res) => {
55
+ const { enabled } = req.body || {};
56
+ if (typeof enabled !== 'boolean') return res.status(400).json({ error: 'Enabled must be a boolean.' });
57
+ res.json(saveClipboardImageEnabled(enabled));
58
+ });
59
+ router.put('/clipboard-deduplicate-minutes', (req, res) => {
60
+ const { minutes } = req.body || {};
61
+ if (typeof minutes !== 'number' || isNaN(minutes) || minutes < 0) return res.status(400).json({ error: 'Minutes must be a non-negative number.' });
62
+ res.json(saveClipboardDeduplicateMinutes(minutes));
63
+ });
39
64
  router.put('/:service-access-token', (req, res) => {
40
65
  const { token } = req.body || {};
41
66
  if (typeof token !== 'string') return res.status(400).json({ error: 'Token must be a string.' });