buddy-workbench 0.1.12 → 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 +1 -1
- package/server/config.js +1 -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.js +2 -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-BfTsErqX.js +0 -521
|
@@ -1,13 +1,125 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
4
|
import { dirname, join } from 'node:path';
|
|
4
5
|
import { promisify } from 'node:util';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import encodeMozjpeg, { init as initMozjpeg } from '../../ui/node_modules/@jsquash/jpeg/encode.js';
|
|
5
8
|
import { paths } from '../config.js';
|
|
6
9
|
import { readSettings } from '../repositories/settings.js';
|
|
7
10
|
|
|
8
11
|
const execFileAsync = promisify(execFile);
|
|
9
12
|
const previewLimit = 2000;
|
|
10
13
|
let lastValue = '';
|
|
14
|
+
let lastImageHash = '';
|
|
15
|
+
let mozjpegWasmModule = null;
|
|
16
|
+
|
|
17
|
+
async function compressMozjpeg(rgbaBuf, width, height, quality = 75) {
|
|
18
|
+
if (!mozjpegWasmModule) {
|
|
19
|
+
const wasmPath = fileURLToPath(import.meta.resolve('../../ui/node_modules/@jsquash/jpeg/codec/enc/mozjpeg_enc.wasm'));
|
|
20
|
+
mozjpegWasmModule = await WebAssembly.compile(readFileSync(wasmPath));
|
|
21
|
+
await initMozjpeg(mozjpegWasmModule);
|
|
22
|
+
}
|
|
23
|
+
const data = new Uint8ClampedArray(rgbaBuf.buffer, rgbaBuf.byteOffset, rgbaBuf.byteLength);
|
|
24
|
+
const arrayBuf = await encodeMozjpeg({ data, width, height }, { quality });
|
|
25
|
+
return Buffer.from(arrayBuf);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function getMacClipboardImageData() {
|
|
29
|
+
const jsaScript = `
|
|
30
|
+
ObjC.import("AppKit");
|
|
31
|
+
var pb = $.NSPasteboard.generalPasteboard;
|
|
32
|
+
var data = pb.dataForType($.NSPasteboardTypePNG);
|
|
33
|
+
if (data.isNil()) {
|
|
34
|
+
data = pb.dataForType($.NSPasteboardTypeTIFF);
|
|
35
|
+
}
|
|
36
|
+
if (!data.isNil()) {
|
|
37
|
+
var image = $.NSImage.alloc.initWithData(data);
|
|
38
|
+
if (!image.isNil()) {
|
|
39
|
+
var tiff = image.TIFFRepresentation;
|
|
40
|
+
var rep = $.NSBitmapImageRep.imageRepWithData(tiff);
|
|
41
|
+
var w = rep.pixelsWide;
|
|
42
|
+
var h = rep.pixelsHigh;
|
|
43
|
+
var rgbaRep = $.NSBitmapImageRep.alloc.initWithBitmapDataPlanesPixelsWidePixelsHighBitsPerSampleSamplesPerPixelHasAlphaIsPlanarColorSpaceNameBytesPerRowBitsPerPixel(
|
|
44
|
+
null, w, h, 8, 4, true, false, $.NSDeviceRGBColorSpace, w * 4, 32
|
|
45
|
+
);
|
|
46
|
+
$.NSGraphicsContext.saveGraphicsState;
|
|
47
|
+
var ctx = $.NSGraphicsContext.graphicsContextWithBitmapImageRep(rgbaRep);
|
|
48
|
+
$.NSGraphicsContext.currentContext = ctx;
|
|
49
|
+
rep.drawInRect($.NSMakeRect(0, 0, w, h));
|
|
50
|
+
$.NSGraphicsContext.restoreGraphicsState;
|
|
51
|
+
var rawBytes = rgbaRep.bitmapData;
|
|
52
|
+
var nsData = $.NSData.dataWithBytesLength(rawBytes, w * h * 4);
|
|
53
|
+
w + ":" + h + ":" + nsData.base64EncodedStringWithOptions(0).js;
|
|
54
|
+
} else {
|
|
55
|
+
"";
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
"";
|
|
59
|
+
}
|
|
60
|
+
`;
|
|
61
|
+
try {
|
|
62
|
+
const { stdout } = await execFileAsync('osascript', ['-l', 'JavaScript', '-e', jsaScript], { timeout: 3000 });
|
|
63
|
+
const str = stdout.trim();
|
|
64
|
+
if (!str) return null;
|
|
65
|
+
const firstColon = str.indexOf(':');
|
|
66
|
+
const secondColon = str.indexOf(':', firstColon + 1);
|
|
67
|
+
if (firstColon === -1 || secondColon === -1) return null;
|
|
68
|
+
const width = parseInt(str.slice(0, firstColon), 10);
|
|
69
|
+
const height = parseInt(str.slice(firstColon + 1, secondColon), 10);
|
|
70
|
+
const base64 = str.slice(secondColon + 1);
|
|
71
|
+
if (!width || !height || !base64) return null;
|
|
72
|
+
return { width, height, rgbaBuf: Buffer.from(base64, 'base64') };
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function getMacClipboardImageBuffer() {
|
|
79
|
+
const swiftCode = `
|
|
80
|
+
import Cocoa
|
|
81
|
+
let pb = NSPasteboard.general
|
|
82
|
+
if let data = pb.data(forType: .png) {
|
|
83
|
+
FileHandle.standardOutput.write(data)
|
|
84
|
+
} else if let data = pb.data(forType: .tiff) {
|
|
85
|
+
if let img = NSImage(data: data),
|
|
86
|
+
let tiff = img.tiffRepresentation,
|
|
87
|
+
let rep = NSBitmapImageRep(data: tiff),
|
|
88
|
+
let png = rep.representation(using: .png, properties: [:]) {
|
|
89
|
+
FileHandle.standardOutput.write(png)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
`;
|
|
93
|
+
try {
|
|
94
|
+
const { stdout } = await execFileAsync('swift', ['-e', swiftCode], { encoding: 'buffer', timeout: 3000 });
|
|
95
|
+
if (stdout && stdout.length > 0) return stdout;
|
|
96
|
+
} catch {
|
|
97
|
+
try {
|
|
98
|
+
const jsaScript = `
|
|
99
|
+
ObjC.import("AppKit");
|
|
100
|
+
var pb = $.NSPasteboard.generalPasteboard;
|
|
101
|
+
var data = pb.dataForType($.NSPasteboardTypePNG);
|
|
102
|
+
if (data.isNil()) {
|
|
103
|
+
var tiffData = pb.dataForType($.NSPasteboardTypeTIFF);
|
|
104
|
+
if (!tiffData.isNil()) {
|
|
105
|
+
var img = $.NSImage.alloc.initWithData(tiffData);
|
|
106
|
+
var rep = $.NSBitmapImageRep.imageRepWithData(img.TIFFRepresentation);
|
|
107
|
+
data = rep.representationUsingTypeProperties($.NSPNGFileType, $());
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!data.isNil()) {
|
|
111
|
+
data.base64EncodedStringWithOptions(0).js;
|
|
112
|
+
} else {
|
|
113
|
+
"";
|
|
114
|
+
}
|
|
115
|
+
`;
|
|
116
|
+
const { stdout } = await execFileAsync('osascript', ['-l', 'JavaScript', '-e', jsaScript], { timeout: 3000 });
|
|
117
|
+
const str = stdout.trim();
|
|
118
|
+
if (str) return Buffer.from(str, 'base64');
|
|
119
|
+
} catch {}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
11
123
|
|
|
12
124
|
function today() { return new Intl.DateTimeFormat('en-CA').format(new Date()); }
|
|
13
125
|
function dayFile(date) { const [year, month, day] = date.split('-'); return join(paths.clipboardDir, year, month, `${day}.json`); }
|
|
@@ -105,61 +217,126 @@ export function deleteClipboardItem(date, id) {
|
|
|
105
217
|
|
|
106
218
|
export async function captureClipboard() {
|
|
107
219
|
if (process.platform !== 'darwin') return;
|
|
108
|
-
|
|
220
|
+
const settings = readSettings();
|
|
221
|
+
if (settings.clipboardEnabled === false) return;
|
|
222
|
+
|
|
223
|
+
// 1. Text capture
|
|
109
224
|
try {
|
|
110
|
-
const { stdout } = await execFileAsync('pbpaste');
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (item.text !== undefined) {
|
|
132
|
-
return item.text === text;
|
|
133
|
-
}
|
|
134
|
-
if (item.contentFile) {
|
|
135
|
-
const prefix = item.preview.slice(0, -1);
|
|
136
|
-
if (text.startsWith(prefix)) {
|
|
137
|
-
try {
|
|
138
|
-
const fullText = readFileSync(join(paths.clipboardDir, 'content', item.contentFile), 'utf8');
|
|
139
|
-
return fullText === text;
|
|
140
|
-
} catch {
|
|
141
|
-
return false;
|
|
225
|
+
const { stdout } = await execFileAsync('pbpaste');
|
|
226
|
+
const text = stdout.trim();
|
|
227
|
+
if (text && text !== lastValue) {
|
|
228
|
+
lastValue = text;
|
|
229
|
+
const deduplicateMinutes = typeof settings.clipboardDeduplicateMinutes === 'number' && !isNaN(settings.clipboardDeduplicateMinutes) ? settings.clipboardDeduplicateMinutes : 60;
|
|
230
|
+
const date = today();
|
|
231
|
+
const items = dayItems(date);
|
|
232
|
+
|
|
233
|
+
let alreadyExists = false;
|
|
234
|
+
if (deduplicateMinutes > 0) {
|
|
235
|
+
const now = Date.now();
|
|
236
|
+
const limit = now - deduplicateMinutes * 60 * 1000;
|
|
237
|
+
const minDate = new Intl.DateTimeFormat('en-CA').format(new Date(limit));
|
|
238
|
+
|
|
239
|
+
let recentItems = [];
|
|
240
|
+
const dates = clipboardDates();
|
|
241
|
+
for (const d of dates) {
|
|
242
|
+
if (d >= minDate) {
|
|
243
|
+
recentItems.push(...dayItems(d));
|
|
244
|
+
} else {
|
|
245
|
+
break;
|
|
142
246
|
}
|
|
143
247
|
}
|
|
248
|
+
|
|
249
|
+
const dedupeWindowItems = recentItems.filter(item => new Date(item.createdAt).getTime() >= limit);
|
|
250
|
+
|
|
251
|
+
alreadyExists = dedupeWindowItems.some(item => {
|
|
252
|
+
if (item.text !== undefined) {
|
|
253
|
+
return item.text === text;
|
|
254
|
+
}
|
|
255
|
+
if (item.contentFile) {
|
|
256
|
+
const prefix = item.preview.slice(0, -1);
|
|
257
|
+
if (text.startsWith(prefix)) {
|
|
258
|
+
try {
|
|
259
|
+
const fullText = readFileSync(join(paths.clipboardDir, 'content', item.contentFile), 'utf8');
|
|
260
|
+
return fullText === text;
|
|
261
|
+
} catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return false;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!alreadyExists) {
|
|
271
|
+
const id = randomUUID(); const isLong = text.length > previewLimit;
|
|
272
|
+
if (isLong) { mkdirSync(join(paths.clipboardDir, 'content'), { recursive: true }); writeFileSync(join(paths.clipboardDir, 'content', id), text, 'utf8'); }
|
|
273
|
+
const item = isLong ? { id, preview: `${text.slice(0, previewLimit)}…`, contentFile: id, createdAt: new Date().toISOString() } : { id, text, createdAt: new Date().toISOString() };
|
|
274
|
+
writeJson(dayFile(date), [item, ...items].slice(0, 200));
|
|
144
275
|
}
|
|
145
|
-
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
if (alreadyExists) return;
|
|
149
|
-
|
|
150
|
-
const id = crypto.randomUUID(); const isLong = text.length > previewLimit;
|
|
151
|
-
if (isLong) { mkdirSync(join(paths.clipboardDir, 'content'), { recursive: true }); writeFileSync(join(paths.clipboardDir, 'content', id), text, 'utf8'); }
|
|
152
|
-
const item = isLong ? { id, preview: `${text.slice(0, previewLimit)}…`, contentFile: id, createdAt: new Date().toISOString() } : { id, text, createdAt: new Date().toISOString() };
|
|
153
|
-
writeJson(dayFile(date), [item, ...items].slice(0, 200));
|
|
276
|
+
}
|
|
154
277
|
} catch {}
|
|
278
|
+
|
|
279
|
+
// 2. Image capture (Mac only & clipboardImageEnabled !== false)
|
|
280
|
+
if (settings.clipboardImageEnabled !== false) {
|
|
281
|
+
try {
|
|
282
|
+
const imageData = await getMacClipboardImageData();
|
|
283
|
+
if (imageData && imageData.rgbaBuf.length > 0) {
|
|
284
|
+
const hash = createHash('md5').update(imageData.rgbaBuf).digest('hex');
|
|
285
|
+
if (hash !== lastImageHash) {
|
|
286
|
+
let isDuplicate = false;
|
|
287
|
+
const dates = clipboardDates();
|
|
288
|
+
for (const d of dates) {
|
|
289
|
+
const dayList = dayItems(d);
|
|
290
|
+
if (dayList.some(item => item.imageHash === hash)) {
|
|
291
|
+
isDuplicate = true;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
lastImageHash = hash;
|
|
297
|
+
if (!isDuplicate) {
|
|
298
|
+
try {
|
|
299
|
+
const mozjpegBuf = await compressMozjpeg(imageData.rgbaBuf, imageData.width, imageData.height, 75);
|
|
300
|
+
saveClipboardImage(mozjpegBuf, 'jpg', hash);
|
|
301
|
+
} catch {
|
|
302
|
+
const rawBuf = await getMacClipboardImageBuffer();
|
|
303
|
+
if (rawBuf) saveClipboardImage(rawBuf, 'png', hash);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
const imageBuffer = await getMacClipboardImageBuffer();
|
|
309
|
+
if (imageBuffer && imageBuffer.length > 0) {
|
|
310
|
+
const hash = createHash('md5').update(imageBuffer).digest('hex');
|
|
311
|
+
if (hash !== lastImageHash) {
|
|
312
|
+
let isDuplicate = false;
|
|
313
|
+
const dates = clipboardDates();
|
|
314
|
+
for (const d of dates) {
|
|
315
|
+
const dayList = dayItems(d);
|
|
316
|
+
if (dayList.some(item => item.imageHash === hash)) {
|
|
317
|
+
isDuplicate = true;
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
lastImageHash = hash;
|
|
323
|
+
if (!isDuplicate) {
|
|
324
|
+
saveClipboardImage(imageBuffer, 'png', hash);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} catch {}
|
|
330
|
+
}
|
|
155
331
|
}
|
|
156
332
|
|
|
157
333
|
export function startClipboardCapture() { captureClipboard(); const timer = setInterval(captureClipboard, 2000); timer.unref(); }
|
|
158
334
|
|
|
159
|
-
export function saveClipboardImage(buffer, ext = 'png') {
|
|
335
|
+
export function saveClipboardImage(buffer, ext = 'png', imageHash = '') {
|
|
160
336
|
const date = today();
|
|
161
|
-
const id =
|
|
337
|
+
const id = randomUUID();
|
|
162
338
|
const filename = `${id}.${ext}`;
|
|
339
|
+
const hash = imageHash || createHash('md5').update(buffer).digest('hex');
|
|
163
340
|
|
|
164
341
|
mkdirSync(join(paths.clipboardDir, 'content'), { recursive: true });
|
|
165
342
|
writeFileSync(join(paths.clipboardDir, 'content', filename), buffer);
|
|
@@ -167,6 +344,7 @@ export function saveClipboardImage(buffer, ext = 'png') {
|
|
|
167
344
|
const item = {
|
|
168
345
|
id,
|
|
169
346
|
imageFile: filename,
|
|
347
|
+
imageHash: hash,
|
|
170
348
|
createdAt: new Date().toISOString()
|
|
171
349
|
};
|
|
172
350
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
|
|
6
|
+
export async function selectDirectory() {
|
|
7
|
+
if (process.platform === 'darwin') {
|
|
8
|
+
try {
|
|
9
|
+
const { stdout } = await execFileAsync('osascript', [
|
|
10
|
+
'-e',
|
|
11
|
+
'POSIX path of (choose folder with prompt "Select Project Folder")'
|
|
12
|
+
]);
|
|
13
|
+
const dirPath = stdout.trim();
|
|
14
|
+
return dirPath ? { path: dirPath, canceled: false } : { path: null, canceled: true };
|
|
15
|
+
} catch {
|
|
16
|
+
return { path: null, canceled: true };
|
|
17
|
+
}
|
|
18
|
+
} else if (process.platform === 'win32') {
|
|
19
|
+
try {
|
|
20
|
+
const psScript = `
|
|
21
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
22
|
+
$dialog = New-Object System.Windows.Forms.FolderBrowserDialog
|
|
23
|
+
$dialog.Description = "Select Project Folder"
|
|
24
|
+
if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
|
|
25
|
+
Write-Output $dialog.SelectedPath
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
const { stdout } = await execFileAsync('powershell', ['-NoProfile', '-Command', psScript]);
|
|
29
|
+
const dirPath = stdout.trim();
|
|
30
|
+
return dirPath ? { path: dirPath, canceled: false } : { path: null, canceled: true };
|
|
31
|
+
} catch {
|
|
32
|
+
return { path: null, canceled: true };
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
try {
|
|
36
|
+
const { stdout } = await execFileAsync('zenity', ['--file-selection', '--directory', '--title=Select Project Folder']);
|
|
37
|
+
const dirPath = stdout.trim();
|
|
38
|
+
return dirPath ? { path: dirPath, canceled: false } : { path: null, canceled: true };
|
|
39
|
+
} catch {
|
|
40
|
+
return { path: null, canceled: true };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
function generateId() {
|
|
4
|
+
return typeof randomUUID === 'function'
|
|
5
|
+
? randomUUID()
|
|
6
|
+
: `pm_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function parseUrl(urlObj) {
|
|
10
|
+
if (!urlObj) return '';
|
|
11
|
+
if (typeof urlObj === 'string') return urlObj;
|
|
12
|
+
if (urlObj.raw) return urlObj.raw;
|
|
13
|
+
|
|
14
|
+
const protocol = urlObj.protocol ? `${urlObj.protocol}://` : '';
|
|
15
|
+
const host = Array.isArray(urlObj.host) ? urlObj.host.join('.') : (urlObj.host || '');
|
|
16
|
+
const path = Array.isArray(urlObj.path) ? `/${urlObj.path.join('/')}` : (urlObj.path || '');
|
|
17
|
+
let query = '';
|
|
18
|
+
if (Array.isArray(urlObj.query) && urlObj.query.length > 0) {
|
|
19
|
+
const activeParams = urlObj.query
|
|
20
|
+
.filter((q) => !q.disabled && q.key)
|
|
21
|
+
.map((q) => `${encodeURIComponent(q.key)}=${encodeURIComponent(q.value || '')}`);
|
|
22
|
+
if (activeParams.length > 0) {
|
|
23
|
+
query = `?${activeParams.join('&')}`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return `${protocol}${host}${path}${query}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseHeaders(headers) {
|
|
30
|
+
if (!Array.isArray(headers)) return [];
|
|
31
|
+
return headers.map((h) => ({
|
|
32
|
+
key: h.key || '',
|
|
33
|
+
value: h.value || '',
|
|
34
|
+
enabled: !h.disabled,
|
|
35
|
+
description: h.description || ''
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseParamsFromUrl(urlStr) {
|
|
40
|
+
if (!urlStr || typeof urlStr !== 'string') return [];
|
|
41
|
+
const queryIndex = urlStr.indexOf('?');
|
|
42
|
+
if (queryIndex === -1) return [];
|
|
43
|
+
const queryString = urlStr.slice(queryIndex + 1);
|
|
44
|
+
const searchParams = new URLSearchParams(queryString);
|
|
45
|
+
const params = [];
|
|
46
|
+
searchParams.forEach((value, key) => {
|
|
47
|
+
params.push({ key, value, enabled: true });
|
|
48
|
+
});
|
|
49
|
+
return params;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseBody(bodyObj) {
|
|
53
|
+
if (!bodyObj) return { mode: 'none', raw: '', json: '', formdata: [], urlencoded: [] };
|
|
54
|
+
const mode = bodyObj.mode || 'none';
|
|
55
|
+
let raw = bodyObj.raw || '';
|
|
56
|
+
let json = '';
|
|
57
|
+
|
|
58
|
+
if (mode === 'raw') {
|
|
59
|
+
if (bodyObj.options?.raw?.language === 'json') {
|
|
60
|
+
json = raw;
|
|
61
|
+
} else {
|
|
62
|
+
try {
|
|
63
|
+
JSON.parse(raw);
|
|
64
|
+
json = raw;
|
|
65
|
+
} catch {
|
|
66
|
+
json = '';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const formdata = Array.isArray(bodyObj.formdata)
|
|
72
|
+
? bodyObj.formdata.map((item) => ({
|
|
73
|
+
key: item.key || '',
|
|
74
|
+
value: item.value || '',
|
|
75
|
+
type: item.type || 'text',
|
|
76
|
+
enabled: !item.disabled
|
|
77
|
+
}))
|
|
78
|
+
: [];
|
|
79
|
+
|
|
80
|
+
const urlencoded = Array.isArray(bodyObj.urlencoded)
|
|
81
|
+
? bodyObj.urlencoded.map((item) => ({
|
|
82
|
+
key: item.key || '',
|
|
83
|
+
value: item.value || '',
|
|
84
|
+
enabled: !item.disabled
|
|
85
|
+
}))
|
|
86
|
+
: [];
|
|
87
|
+
|
|
88
|
+
return { mode, raw, json, formdata, urlencoded };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseAuth(authObj) {
|
|
92
|
+
if (!authObj || !authObj.type) return { type: 'none', token: '', username: '', password: '' };
|
|
93
|
+
const type = authObj.type;
|
|
94
|
+
if (type === 'bearer') {
|
|
95
|
+
const tokenObj = Array.isArray(authObj.bearer) ? authObj.bearer.find((b) => b.key === 'token') : null;
|
|
96
|
+
return { type: 'bearer', token: tokenObj?.value || '', username: '', password: '' };
|
|
97
|
+
}
|
|
98
|
+
if (type === 'basic') {
|
|
99
|
+
const userObj = Array.isArray(authObj.basic) ? authObj.basic.find((b) => b.key === 'username') : null;
|
|
100
|
+
const passObj = Array.isArray(authObj.basic) ? authObj.basic.find((b) => b.key === 'password') : null;
|
|
101
|
+
return { type: 'basic', token: '', username: userObj?.value || '', password: passObj?.value || '' };
|
|
102
|
+
}
|
|
103
|
+
return { type: 'none', token: '', username: '', password: '' };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseItem(item, folderId = null) {
|
|
107
|
+
const isFolder = Array.isArray(item.item);
|
|
108
|
+
if (isFolder) {
|
|
109
|
+
const currentFolderId = generateId();
|
|
110
|
+
const folderNode = {
|
|
111
|
+
id: currentFolderId,
|
|
112
|
+
name: item.name || 'Folder',
|
|
113
|
+
description: item.description || '',
|
|
114
|
+
parentId: folderId,
|
|
115
|
+
isFolder: true
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
let children = [];
|
|
119
|
+
item.item.forEach((subItem) => {
|
|
120
|
+
const res = parseItem(subItem, currentFolderId);
|
|
121
|
+
if (Array.isArray(res)) {
|
|
122
|
+
children.push(...res);
|
|
123
|
+
} else {
|
|
124
|
+
children.push(res);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return [folderNode, ...children];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Request item
|
|
132
|
+
const reqObj = item.request || {};
|
|
133
|
+
const rawUrl = parseUrl(reqObj.url);
|
|
134
|
+
const parsedParams = parseParamsFromUrl(rawUrl);
|
|
135
|
+
|
|
136
|
+
const requestNode = {
|
|
137
|
+
id: generateId(),
|
|
138
|
+
name: item.name || 'Request',
|
|
139
|
+
description: typeof reqObj.description === 'string' ? reqObj.description : (item.description || ''),
|
|
140
|
+
parentId: folderId,
|
|
141
|
+
isFolder: false,
|
|
142
|
+
method: (reqObj.method || 'GET').toUpperCase(),
|
|
143
|
+
url: rawUrl,
|
|
144
|
+
params: parsedParams,
|
|
145
|
+
headers: parseHeaders(reqObj.header),
|
|
146
|
+
body: parseBody(reqObj.body),
|
|
147
|
+
auth: parseAuth(reqObj.auth)
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return requestNode;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function parseCurlCommand(curlStr) {
|
|
154
|
+
if (!curlStr || typeof curlStr !== 'string') throw new Error('Empty cURL command');
|
|
155
|
+
|
|
156
|
+
const cleaned = curlStr.replace(/\\\r?\n/g, ' ').trim();
|
|
157
|
+
const args = [];
|
|
158
|
+
const regex = /"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|(\S+)/g;
|
|
159
|
+
let match;
|
|
160
|
+
while ((match = regex.exec(cleaned)) !== null) {
|
|
161
|
+
if (match[1] !== undefined) {
|
|
162
|
+
args.push(match[1].replace(/\\"/g, '"'));
|
|
163
|
+
} else if (match[2] !== undefined) {
|
|
164
|
+
args.push(match[2].replace(/\\'/g, "'"));
|
|
165
|
+
} else {
|
|
166
|
+
args.push(match[3]);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (args.length === 0) throw new Error('Invalid cURL command');
|
|
171
|
+
|
|
172
|
+
let method = null;
|
|
173
|
+
let url = '';
|
|
174
|
+
const headers = [];
|
|
175
|
+
let bodyRaw = '';
|
|
176
|
+
let authInfo = { type: 'none' };
|
|
177
|
+
|
|
178
|
+
for (let i = 0; i < args.length; i++) {
|
|
179
|
+
const arg = args[i];
|
|
180
|
+
|
|
181
|
+
if (arg === '-X' || arg === '--request') {
|
|
182
|
+
if (args[i + 1]) {
|
|
183
|
+
method = args[i + 1].toUpperCase();
|
|
184
|
+
i++;
|
|
185
|
+
}
|
|
186
|
+
} else if (arg === '-H' || arg === '--header') {
|
|
187
|
+
if (args[i + 1]) {
|
|
188
|
+
const headerStr = args[i + 1];
|
|
189
|
+
const colonIdx = headerStr.indexOf(':');
|
|
190
|
+
if (colonIdx > 0) {
|
|
191
|
+
const key = headerStr.slice(0, colonIdx).trim();
|
|
192
|
+
const value = headerStr.slice(colonIdx + 1).trim();
|
|
193
|
+
headers.push({ key, value, disabled: false });
|
|
194
|
+
}
|
|
195
|
+
i++;
|
|
196
|
+
}
|
|
197
|
+
} else if (['-d', '--data', '--data-raw', '--data-binary'].includes(arg)) {
|
|
198
|
+
if (args[i + 1]) {
|
|
199
|
+
bodyRaw = args[i + 1];
|
|
200
|
+
i++;
|
|
201
|
+
}
|
|
202
|
+
} else if (arg === '-u' || arg === '--user') {
|
|
203
|
+
if (args[i + 1]) {
|
|
204
|
+
const userPass = args[i + 1];
|
|
205
|
+
const [u, p] = userPass.split(':');
|
|
206
|
+
authInfo = { type: 'basic', username: u || '', password: p || '' };
|
|
207
|
+
i++;
|
|
208
|
+
}
|
|
209
|
+
} else if (arg === '--url') {
|
|
210
|
+
if (args[i + 1]) {
|
|
211
|
+
url = args[i + 1];
|
|
212
|
+
i++;
|
|
213
|
+
}
|
|
214
|
+
} else if (!arg.startsWith('-') && arg.toLowerCase() !== 'curl' && !url) {
|
|
215
|
+
url = arg;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!url) {
|
|
220
|
+
throw new Error('Could not find target URL in cURL command');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!method) {
|
|
224
|
+
method = bodyRaw ? 'POST' : 'GET';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let reqName = `${method} Request`;
|
|
228
|
+
try {
|
|
229
|
+
const parsedUrl = new URL(url.startsWith('http') ? url : `http://${url}`);
|
|
230
|
+
const pathname = parsedUrl.pathname;
|
|
231
|
+
if (pathname && pathname !== '/') {
|
|
232
|
+
const parts = pathname.split('/').filter(Boolean);
|
|
233
|
+
if (parts.length > 0) reqName = `${method} /${parts[parts.length - 1]}`;
|
|
234
|
+
} else {
|
|
235
|
+
reqName = `${method} ${parsedUrl.hostname}`;
|
|
236
|
+
}
|
|
237
|
+
} catch {}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
info: { name: `cURL: ${reqName}` },
|
|
241
|
+
item: [{
|
|
242
|
+
name: reqName,
|
|
243
|
+
request: {
|
|
244
|
+
method,
|
|
245
|
+
url,
|
|
246
|
+
header: headers,
|
|
247
|
+
body: bodyRaw ? { mode: 'raw', raw: bodyRaw } : undefined,
|
|
248
|
+
auth: authInfo.type !== 'none' ? authInfo : undefined
|
|
249
|
+
}
|
|
250
|
+
}]
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function parsePostmanCollection(jsonInput) {
|
|
255
|
+
let collectionData;
|
|
256
|
+
if (typeof jsonInput === 'string') {
|
|
257
|
+
const trimmed = jsonInput.trim();
|
|
258
|
+
if (trimmed.toLowerCase().startsWith('curl') || (trimmed.includes('-H') && trimmed.includes('http'))) {
|
|
259
|
+
collectionData = parseCurlCommand(trimmed);
|
|
260
|
+
} else {
|
|
261
|
+
collectionData = JSON.parse(trimmed);
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
collectionData = jsonInput;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const info = collectionData.info || {};
|
|
268
|
+
const items = Array.isArray(collectionData.item) ? collectionData.item : [];
|
|
269
|
+
|
|
270
|
+
const collectionId = generateId();
|
|
271
|
+
const collectionName = info.name || 'Imported Collection';
|
|
272
|
+
const description = info.description || '';
|
|
273
|
+
|
|
274
|
+
const variables = Array.isArray(collectionData.variable)
|
|
275
|
+
? collectionData.variable.map((v) => ({
|
|
276
|
+
key: v.key || '',
|
|
277
|
+
value: v.value || '',
|
|
278
|
+
enabled: !v.disabled
|
|
279
|
+
}))
|
|
280
|
+
: [];
|
|
281
|
+
|
|
282
|
+
const parsedItems = [];
|
|
283
|
+
items.forEach((it) => {
|
|
284
|
+
const res = parseItem(it, null);
|
|
285
|
+
if (Array.isArray(res)) {
|
|
286
|
+
parsedItems.push(...res);
|
|
287
|
+
} else {
|
|
288
|
+
parsedItems.push(res);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
id: collectionId,
|
|
294
|
+
name: collectionName,
|
|
295
|
+
description: typeof description === 'string' ? description : '',
|
|
296
|
+
variables,
|
|
297
|
+
items: parsedItems,
|
|
298
|
+
createdAt: new Date().toISOString()
|
|
299
|
+
};
|
|
300
|
+
}
|
package/server.js
CHANGED
|
@@ -17,6 +17,7 @@ import jiraFiltersRoutes from './server/routes/jira-filters.js';
|
|
|
17
17
|
import todoRoutes from './server/routes/todos.js';
|
|
18
18
|
import staticPagesRoutes from './server/routes/static-pages.js';
|
|
19
19
|
import errorRoutes from './server/routes/errors.js';
|
|
20
|
+
import postmanRoutes from './server/routes/postman.js';
|
|
20
21
|
import { addErrorRecord } from './server/repositories/errors.js';
|
|
21
22
|
import { startClipboardCapture } from './server/services/clipboard-history.js';
|
|
22
23
|
|
|
@@ -69,6 +70,7 @@ app.use('/api/jira-filters', jiraFiltersRoutes);
|
|
|
69
70
|
app.use('/api/todos', todoRoutes);
|
|
70
71
|
app.use('/api/static-pages', staticPagesRoutes);
|
|
71
72
|
app.use('/api/errors', errorRoutes);
|
|
73
|
+
app.use('/api/postman', postmanRoutes);
|
|
72
74
|
|
|
73
75
|
app.use((err, req, res, _next) => {
|
|
74
76
|
addErrorRecord({
|