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 +3 -2
- package/pages/.gitkeep +1 -0
- package/pages/welcome/index.html +72 -0
- package/server/config.js +3 -0
- package/server/repositories/postman.js +31 -0
- package/server/repositories/settings.js +21 -1
- package/server/routes/postman.js +283 -0
- package/server/routes/settings.js +27 -2
- package/server/services/browser.js +70 -0
- package/server/services/clipboard-history.js +222 -44
- package/server/services/dialog.js +43 -0
- package/server/services/postman-parser.js +300 -0
- package/server/services/process-manager.js +29 -11
- package/server.js +5 -0
- package/ui/dist/assets/{index-BuBK4DeN.css → index-B9y46rtV.css} +1 -1
- package/ui/dist/assets/index-yM779_4l.js +524 -0
- package/ui/dist/index.html +10 -4
- package/ui/dist/assets/index-CdpFZDzz.js +0 -521
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "buddy-workbench",
|
|
3
|
-
"version": "0.1.
|
|
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 > 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
|
-
|
|
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
|
+
}
|