buddy-workbench 0.1.11 → 0.1.13

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.11",
3
+ "version": "0.1.13",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,8 @@
10
10
  "server.js",
11
11
  "server/",
12
12
  "ui/dist/",
13
- "plugins/"
13
+ "plugins/",
14
+ "pages/"
14
15
  ],
15
16
  "scripts": {
16
17
  "start": "exec node server.js",
package/pages/.gitkeep ADDED
@@ -0,0 +1 @@
1
+ # Keep pages directory
@@ -0,0 +1,72 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Welcome - DevBuddy Static Pages</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0f172a;
10
+ --card-bg: #1e293b;
11
+ --text: #f8fafc;
12
+ --text-muted: #94a3b8;
13
+ --accent: #38bdf8;
14
+ --border: #334155;
15
+ }
16
+ body {
17
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
18
+ background: var(--bg);
19
+ color: var(--text);
20
+ display: flex;
21
+ justify-content: center;
22
+ align-items: center;
23
+ min-height: 100vh;
24
+ margin: 0;
25
+ padding: 24px;
26
+ box-sizing: border-box;
27
+ }
28
+ .card {
29
+ background: var(--card-bg);
30
+ padding: 32px;
31
+ border-radius: 16px;
32
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
33
+ border: 1px solid var(--border);
34
+ text-align: center;
35
+ max-width: 520px;
36
+ width: 100%;
37
+ }
38
+ .icon {
39
+ font-size: 48px;
40
+ margin-bottom: 16px;
41
+ }
42
+ h1 {
43
+ color: var(--accent);
44
+ margin-top: 0;
45
+ margin-bottom: 12px;
46
+ font-size: 24px;
47
+ }
48
+ p {
49
+ color: var(--text-muted);
50
+ line-height: 1.6;
51
+ margin-bottom: 20px;
52
+ }
53
+ code {
54
+ background: rgba(15, 23, 42, 0.8);
55
+ padding: 4px 8px;
56
+ border-radius: 6px;
57
+ color: #f43f5e;
58
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
59
+ font-size: 0.9em;
60
+ border: 1px solid var(--border);
61
+ }
62
+ </style>
63
+ </head>
64
+ <body>
65
+ <div class="card">
66
+ <div class="icon">🚀</div>
67
+ <h1>Static Server Active</h1>
68
+ <p>Place your HTML, CSS, and JS files inside the <code>pages/</code> directory in your workspace.</p>
69
+ <p>Configure this page in Settings &gt; Static Pages with URL <code>/pages/welcome/index.html</code></p>
70
+ </div>
71
+ </body>
72
+ </html>
package/server/config.js CHANGED
@@ -11,8 +11,11 @@ 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'),
18
+ pages: join(root, 'pages'),
19
+ dataPages: join(root, 'data', 'pages'),
17
20
  ui: join(root, 'ui', 'dist')
18
21
  };
@@ -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,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.' });
@@ -1,4 +1,7 @@
1
1
  import { execFile } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
2
5
  import { readSettings } from '../repositories/settings.js';
3
6
 
4
7
  export function openInSystemBrowser(url) {
@@ -26,3 +29,70 @@ export function openInSystemBrowser(url) {
26
29
  execFile('xdg-open', [url]);
27
30
  }
28
31
  }
32
+
33
+ function findMacAppPath(appNames) {
34
+ const baseDirs = [join(homedir(), 'Applications'), '/Applications'];
35
+ for (const dir of baseDirs) {
36
+ for (const name of appNames) {
37
+ const appPath = join(dir, `${name}.app`);
38
+ if (existsSync(appPath)) return appPath;
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+
44
+ export function openInSystemEditor(path, location = '') {
45
+ if (!path || typeof path !== 'string') return;
46
+ const settings = readSettings();
47
+ const editor = settings.defaultEditor || 'vscode';
48
+
49
+ if (process.platform === 'darwin') {
50
+ if (editor === 'idea') {
51
+ const ideaApp = findMacAppPath(['IntelliJ IDEA', 'IntelliJ IDEA CE']);
52
+ if (ideaApp) {
53
+ execFile('open', ['-a', ideaApp, path]);
54
+ } else {
55
+ execFile('open', ['-a', 'IntelliJ IDEA', path]);
56
+ }
57
+ } else if (editor === 'devin') {
58
+ const devinApp = findMacAppPath(['Devin']);
59
+ if (devinApp) {
60
+ execFile('open', ['-a', devinApp, path]);
61
+ } else {
62
+ execFile('open', ['-a', 'Devin', path]);
63
+ }
64
+ } else {
65
+ // macOS VS Code:
66
+ const candidatePaths = [
67
+ join(homedir(), 'Applications', 'Visual Studio Code.app', 'Contents', 'Resources', 'app', 'bin', 'code'),
68
+ '/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code'
69
+ ];
70
+ const embeddedCodePath = candidatePaths.find((p) => existsSync(p));
71
+
72
+ if (embeddedCodePath) {
73
+ execFile(embeddedCodePath, ['-n', path]);
74
+ } else {
75
+ execFile('code', ['-n', path], (err) => {
76
+ if (err) {
77
+ const vsCodeApp = findMacAppPath(['Visual Studio Code']) || 'Visual Studio Code';
78
+ execFile('open', ['-a', vsCodeApp, path]);
79
+ }
80
+ });
81
+ }
82
+ }
83
+ } else if (process.platform === 'win32') {
84
+ if (editor === 'vscode') {
85
+ execFile('cmd', ['/c', 'code', '-n', path]);
86
+ } else if (editor === 'idea') {
87
+ execFile('cmd', ['/c', 'idea', path]);
88
+ } else {
89
+ execFile('cmd', ['/c', 'start', '', path]);
90
+ }
91
+ } else {
92
+ if (editor === 'vscode') {
93
+ execFile('code', ['-n', [path, location].filter(Boolean).join(':')]);
94
+ } else {
95
+ execFile('xdg-open', [path]);
96
+ }
97
+ }
98
+ }