buddy-workbench 0.2.0 → 0.2.1
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/repositories/settings.js +19 -0
- package/server/routes/settings.js +8 -1
- package/server/services/mac-clipboard-helper.swift +32 -15
- package/ui/dist/assets/{index-C5cAJkeS.js → index-BnENK4hw.js} +152 -152
- package/ui/dist/assets/index-CeG26AqO.css +1 -0
- package/ui/dist/assets/{xmindAdapter-B39G3oq_.js → xmindAdapter-B-Km7qAa.js} +1 -1
- package/ui/dist/index.html +2 -2
- package/ui/dist/sw.js +2 -2
- package/ui/dist/assets/index-Bj6A5tYm.css +0 -1
package/package.json
CHANGED
|
@@ -23,6 +23,9 @@ export function settingsStatus() {
|
|
|
23
23
|
clipboardEnabled: settings.clipboardEnabled !== false,
|
|
24
24
|
clipboardImageEnabled: settings.clipboardImageEnabled !== false,
|
|
25
25
|
clipboardDeduplicateMinutes: typeof settings.clipboardDeduplicateMinutes === 'number' && !isNaN(settings.clipboardDeduplicateMinutes) ? settings.clipboardDeduplicateMinutes : 60,
|
|
26
|
+
mindMapNodeMaxWidth: Number.isInteger(settings.mindMapNodeMaxWidth) && settings.mindMapNodeMaxWidth >= 120 && settings.mindMapNodeMaxWidth <= 800
|
|
27
|
+
? settings.mindMapNodeMaxWidth
|
|
28
|
+
: null,
|
|
26
29
|
dataDir: paths.dataDir
|
|
27
30
|
};
|
|
28
31
|
}
|
|
@@ -105,6 +108,22 @@ export function saveClipboardDeduplicateMinutes(minutes) {
|
|
|
105
108
|
return settingsStatus();
|
|
106
109
|
}
|
|
107
110
|
|
|
111
|
+
export function saveMindMapNodeMaxWidth(width) {
|
|
112
|
+
const settings = readSettings();
|
|
113
|
+
if (width === null) {
|
|
114
|
+
delete settings.mindMapNodeMaxWidth;
|
|
115
|
+
} else {
|
|
116
|
+
const value = Number(width);
|
|
117
|
+
if (!Number.isInteger(value) || value < 120 || value > 800) {
|
|
118
|
+
throw new Error('Mind map node maximum width must be an integer between 120 and 800.');
|
|
119
|
+
}
|
|
120
|
+
settings.mindMapNodeMaxWidth = value;
|
|
121
|
+
}
|
|
122
|
+
mkdirSync(dirname(paths.settings), { recursive: true });
|
|
123
|
+
writeFileSync(paths.settings, JSON.stringify(settings, null, 2), { mode: 0o600 });
|
|
124
|
+
return settingsStatus();
|
|
125
|
+
}
|
|
126
|
+
|
|
108
127
|
export function readPrReviewRules(defaultRules) {
|
|
109
128
|
const settings = readSettings();
|
|
110
129
|
const savedRules = settings.prReviewRules && typeof settings.prReviewRules === 'object' ? settings.prReviewRules : {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { Router } from 'express';
|
|
3
3
|
import { paths } from '../config.js';
|
|
4
|
-
import { saveAccessToken, saveAiApiHost, saveClipboardDeduplicateMinutes, saveClipboardEnabled, saveClipboardImageEnabled, saveDefaultBrowser, saveDefaultEditor, saveDomain, saveJiraIssuePrefix, saveTheme, settingsStatus } from '../repositories/settings.js';
|
|
4
|
+
import { saveAccessToken, saveAiApiHost, saveClipboardDeduplicateMinutes, saveClipboardEnabled, saveClipboardImageEnabled, saveDefaultBrowser, saveDefaultEditor, saveDomain, saveJiraIssuePrefix, saveMindMapNodeMaxWidth, saveTheme, settingsStatus } from '../repositories/settings.js';
|
|
5
5
|
import { getNpmPackageVersions, installNvmVersion, manageGlobalNpmPackage, readDevConfigurations, readNvmConfiguration, saveDevConfigurations, searchNpmPackages, useSuggestedNpmPrefix } from '../services/dev-configurations.js';
|
|
6
6
|
import { openInSystemBrowser, openInSystemEditor, openInSystemFolder, revealInSystemFolder } from '../services/browser.js';
|
|
7
7
|
import { selectDirectory, selectFile } from '../services/dialog.js';
|
|
@@ -149,6 +149,13 @@ router.put('/clipboard-deduplicate-minutes', (req, res) => {
|
|
|
149
149
|
if (typeof minutes !== 'number' || isNaN(minutes) || minutes < 0) return res.status(400).json({ error: 'Minutes must be a non-negative number.' });
|
|
150
150
|
res.json(saveClipboardDeduplicateMinutes(minutes));
|
|
151
151
|
});
|
|
152
|
+
router.put('/mind-map-node-max-width', (req, res) => {
|
|
153
|
+
const { width } = req.body || {};
|
|
154
|
+
if (width !== null && (typeof width !== 'number' || !Number.isInteger(width) || width < 120 || width > 800)) {
|
|
155
|
+
return res.status(400).json({ error: 'Width must be an integer between 120 and 800, or null to use the layout default.' });
|
|
156
|
+
}
|
|
157
|
+
try { res.json(saveMindMapNodeMaxWidth(width)); } catch (error) { res.status(400).json({ error: error.message }); }
|
|
158
|
+
});
|
|
152
159
|
router.put('/:service-access-token', (req, res) => {
|
|
153
160
|
const { token } = req.body || {};
|
|
154
161
|
if (typeof token !== 'string') return res.status(400).json({ error: 'Token must be a string.' });
|
|
@@ -2,12 +2,14 @@ import AppKit
|
|
|
2
2
|
import CoreGraphics
|
|
3
3
|
import ImageIO
|
|
4
4
|
import Foundation
|
|
5
|
+
import UniformTypeIdentifiers
|
|
5
6
|
|
|
6
7
|
// Keep this process alive so Swift is compiled once. Pasteboard image bytes
|
|
7
8
|
// are requested only after the pasteboard ownership changes.
|
|
8
9
|
let outputDirectory = URL(fileURLWithPath: CommandLine.arguments[1], isDirectory: true)
|
|
9
10
|
let maxInputBytes = 100 * 1024 * 1024
|
|
10
11
|
let maxPixelCount = 40_000_000
|
|
12
|
+
let maxCaptureAttempts = 40
|
|
11
13
|
let imageTypes: [NSPasteboard.PasteboardType] = [
|
|
12
14
|
.png,
|
|
13
15
|
NSPasteboard.PasteboardType("public.jpeg"),
|
|
@@ -15,22 +17,37 @@ let imageTypes: [NSPasteboard.PasteboardType] = [
|
|
|
15
17
|
NSPasteboard.PasteboardType("public.heic")
|
|
16
18
|
]
|
|
17
19
|
|
|
20
|
+
func isImageType(_ type: NSPasteboard.PasteboardType) -> Bool {
|
|
21
|
+
if imageTypes.contains(type) { return true }
|
|
22
|
+
if let uniformType = UTType(type.rawValue), uniformType.conforms(to: .image) { return true }
|
|
23
|
+
return type.rawValue.localizedCaseInsensitiveContains("image")
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
func emit(_ value: [String: Any]) {
|
|
19
27
|
guard let data = try? JSONSerialization.data(withJSONObject: value),
|
|
20
28
|
let line = String(data: data, encoding: .utf8) else { return }
|
|
21
29
|
FileHandle.standardOutput.write(Data((line + "\n").utf8))
|
|
22
30
|
}
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
enum CaptureResult: Equatable {
|
|
33
|
+
case complete
|
|
34
|
+
case retry
|
|
35
|
+
case waitingForImageType
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Browser clipboard owners can publish the image type or fulfill its data
|
|
39
|
+
// asynchronously, so keep checking the same pasteboard change for a short time.
|
|
40
|
+
func captureImage(_ pasteboard: NSPasteboard, changeCount: Int, startedAt: Date) -> CaptureResult {
|
|
41
|
+
let availableTypes = pasteboard.types ?? []
|
|
42
|
+
let type = imageTypes.first(where: { availableTypes.contains($0) })
|
|
43
|
+
?? availableTypes.first(where: isImageType)
|
|
44
|
+
guard let type else { return .waitingForImageType }
|
|
28
45
|
guard let input = pasteboard.data(forType: type) else {
|
|
29
|
-
return
|
|
46
|
+
return .retry
|
|
30
47
|
}
|
|
31
48
|
guard !input.isEmpty, input.count <= maxInputBytes else {
|
|
32
49
|
emit(["kind": "error", "stage": "image validation", "message": "Image data was empty or exceeded the 100 MiB input limit.", "changeCount": changeCount, "elapsedMs": Int(Date().timeIntervalSince(startedAt) * 1000)])
|
|
33
|
-
return
|
|
50
|
+
return .complete
|
|
34
51
|
}
|
|
35
52
|
guard let source = CGImageSourceCreateWithData(input as CFData, nil),
|
|
36
53
|
let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any],
|
|
@@ -39,22 +56,22 @@ func captureImage(_ pasteboard: NSPasteboard, changeCount: Int, startedAt: Date)
|
|
|
39
56
|
width > 0, height > 0,
|
|
40
57
|
width <= maxPixelCount / height else {
|
|
41
58
|
emit(["kind": "error", "stage": "image validation", "message": "Image dimensions were invalid or exceeded the 40 megapixel limit.", "changeCount": changeCount, "elapsedMs": Int(Date().timeIntervalSince(startedAt) * 1000)])
|
|
42
|
-
return
|
|
59
|
+
return .complete
|
|
43
60
|
}
|
|
44
61
|
guard let image = CGImageSourceCreateImageAtIndex(source, 0, [kCGImageSourceShouldCache: true] as CFDictionary) else {
|
|
45
62
|
emit(["kind": "error", "stage": "image decode", "message": "ImageIO could not decode the clipboard image.", "changeCount": changeCount, "elapsedMs": Int(Date().timeIntervalSince(startedAt) * 1000)])
|
|
46
|
-
return
|
|
63
|
+
return .complete
|
|
47
64
|
}
|
|
48
65
|
|
|
49
66
|
let jpeg = NSMutableData()
|
|
50
67
|
guard let destination = CGImageDestinationCreateWithData(jpeg, "public.jpeg" as CFString, 1, nil) else {
|
|
51
68
|
emit(["kind": "error", "stage": "JPEG encode", "message": "ImageIO could not create a JPEG encoder.", "changeCount": changeCount, "elapsedMs": Int(Date().timeIntervalSince(startedAt) * 1000)])
|
|
52
|
-
return
|
|
69
|
+
return .complete
|
|
53
70
|
}
|
|
54
71
|
CGImageDestinationAddImage(destination, image, [kCGImageDestinationLossyCompressionQuality: 0.75] as CFDictionary)
|
|
55
72
|
guard CGImageDestinationFinalize(destination), jpeg.length > 0 else {
|
|
56
73
|
emit(["kind": "error", "stage": "JPEG encode", "message": "ImageIO could not finish JPEG encoding.", "changeCount": changeCount, "elapsedMs": Int(Date().timeIntervalSince(startedAt) * 1000)])
|
|
57
|
-
return
|
|
74
|
+
return .complete
|
|
58
75
|
}
|
|
59
76
|
|
|
60
77
|
let filename = "capture-\(UUID().uuidString).jpg"
|
|
@@ -76,7 +93,7 @@ func captureImage(_ pasteboard: NSPasteboard, changeCount: Int, startedAt: Date)
|
|
|
76
93
|
try? FileManager.default.removeItem(at: outputURL)
|
|
77
94
|
emit(["kind": "error", "stage": "JPEG save", "message": "Could not write the temporary JPEG.", "changeCount": changeCount, "elapsedMs": Int(Date().timeIntervalSince(startedAt) * 1000)])
|
|
78
95
|
}
|
|
79
|
-
return
|
|
96
|
+
return .complete
|
|
80
97
|
}
|
|
81
98
|
|
|
82
99
|
emit(["kind": "ready"])
|
|
@@ -99,11 +116,11 @@ while true {
|
|
|
99
116
|
|
|
100
117
|
if let pending = pendingChangeCount {
|
|
101
118
|
attemptsForPendingChange += 1
|
|
102
|
-
let
|
|
103
|
-
if
|
|
104
|
-
emit(["kind": "error", "stage": "pasteboard read", "message": "Image data remained unavailable after
|
|
119
|
+
let result = captureImage(pasteboard, changeCount: pending, startedAt: pendingStartedAt)
|
|
120
|
+
if result == .retry && attemptsForPendingChange >= maxCaptureAttempts {
|
|
121
|
+
emit(["kind": "error", "stage": "pasteboard read", "message": "Image data remained unavailable after \(maxCaptureAttempts) retries.", "changeCount": pending, "elapsedMs": Int(Date().timeIntervalSince(pendingStartedAt) * 1000)])
|
|
105
122
|
}
|
|
106
|
-
if
|
|
123
|
+
if result == .complete || attemptsForPendingChange >= maxCaptureAttempts {
|
|
107
124
|
pendingChangeCount = nil
|
|
108
125
|
}
|
|
109
126
|
}
|