buddy-workbench 0.1.87 → 0.1.89

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.87",
3
+ "version": "0.1.89",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,9 @@
1
1
  import crypto from 'node:crypto';
2
2
  import https from 'node:https';
3
+ import { join } from 'node:path';
3
4
  import axios from 'axios';
4
5
  import { Router } from 'express';
6
+ import { paths } from '../config.js';
5
7
  import { readSettings } from '../repositories/settings.js';
6
8
  import { listMindMaps, saveMindMaps } from '../repositories/mind-maps.js';
7
9
 
@@ -13,6 +15,7 @@ const httpClient = axios.create({
13
15
  });
14
16
 
15
17
  const makeId = () => crypto.randomUUID();
18
+ const getMindMapFilePath = (id) => join(paths.mindMaps, `${encodeURIComponent(String(id))}.json`);
16
19
 
17
20
  function getJiraHost(domain) {
18
21
  const clean = String(domain || '').replace(/^https?:\/\//i, '').replace(/\/+$/, '');
@@ -106,13 +109,13 @@ function normalizeMindMap(input, { id = makeId(), createdAt = new Date().toISOSt
106
109
  }
107
110
 
108
111
  router.get('/', (_req, res) => {
109
- res.json(listMindMaps());
112
+ res.json(listMindMaps().map((mindMap) => ({ ...mindMap, filePath: getMindMapFilePath(mindMap.id) })));
110
113
  });
111
114
 
112
115
  router.get('/:id', (req, res) => {
113
116
  const mindMap = listMindMaps().find((item) => String(item.id) === String(req.params.id));
114
117
  if (!mindMap) return res.status(404).json({ error: 'Mind map not found.' });
115
- res.json(mindMap);
118
+ res.json({ ...mindMap, filePath: getMindMapFilePath(mindMap.id) });
116
119
  });
117
120
 
118
121
  router.post('/jira-issues/resolve', async (req, res) => {
@@ -129,7 +132,7 @@ router.post('/', (req, res) => {
129
132
  const mindMaps = listMindMaps();
130
133
  mindMaps.unshift(mindMap);
131
134
  saveMindMaps(mindMaps);
132
- res.status(201).json(mindMap);
135
+ res.status(201).json({ ...mindMap, filePath: getMindMapFilePath(mindMap.id) });
133
136
  });
134
137
 
135
138
  router.put('/:id', (req, res) => {
@@ -144,7 +147,7 @@ router.put('/:id', (req, res) => {
144
147
  if (!updated) return res.status(400).json({ error: 'A root node is required.' });
145
148
  mindMaps[index] = updated;
146
149
  saveMindMaps(mindMaps);
147
- res.json(updated);
150
+ res.json({ ...updated, filePath: getMindMapFilePath(updated.id) });
148
151
  });
149
152
 
150
153
  router.delete('/:id', (req, res) => {
@@ -1,9 +1,9 @@
1
- import { existsSync, mkdirSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, statSync } from 'node:fs';
2
2
  import { Router } from 'express';
3
3
  import { paths } from '../config.js';
4
4
  import { saveAccessToken, saveAiApiHost, saveClipboardDeduplicateMinutes, saveClipboardEnabled, saveClipboardImageEnabled, saveDefaultBrowser, saveDefaultEditor, saveDomain, saveJiraIssuePrefix, saveTheme, settingsStatus } from '../repositories/settings.js';
5
5
  import { getNpmPackageVersions, installNvmVersion, manageGlobalNpmPackage, readDevConfigurations, readNvmConfiguration, saveDevConfigurations, searchNpmPackages, useSuggestedNpmPrefix } from '../services/dev-configurations.js';
6
- import { openInSystemBrowser, openInSystemEditor, openInSystemFolder } from '../services/browser.js';
6
+ import { openInSystemBrowser, openInSystemEditor, openInSystemFolder, revealInSystemFolder } from '../services/browser.js';
7
7
  import { selectDirectory, selectFile } from '../services/dialog.js';
8
8
 
9
9
  const router = Router();
@@ -91,10 +91,15 @@ router.post('/open-editor', (req, res) => {
91
91
  router.post('/open-folder', (req, res) => {
92
92
  const { path } = req.body || {};
93
93
  const targetPath = (typeof path === 'string' && path.trim()) ? path.trim() : paths.dataDir;
94
- if (!existsSync(targetPath)) {
95
- mkdirSync(targetPath, { recursive: true });
94
+ let folderPath = targetPath;
95
+ if (existsSync(targetPath) && statSync(targetPath).isFile()) {
96
+ revealInSystemFolder(targetPath);
97
+ return res.json({ success: true });
96
98
  }
97
- openInSystemFolder(targetPath);
99
+ if (!existsSync(folderPath)) {
100
+ mkdirSync(folderPath, { recursive: true });
101
+ }
102
+ openInSystemFolder(folderPath);
98
103
  res.json({ success: true });
99
104
  });
100
105
  router.post('/open-data-dir', (req, res) => {
@@ -139,13 +139,17 @@ async function readNpmPackageMetadata() {
139
139
 
140
140
  async function checkForUpdates() {
141
141
  const metadata = await readNpmPackageMetadata();
142
- const latestVersion = String(metadata.version).replace(/^v/i, '');
142
+ const releases = buildReleaseList(metadata);
143
+ // A release is only visible after the 48-hour safety window. Keep the
144
+ // summary and the release list on the same source of truth so an update
145
+ // can never target a newer, still-hidden npm release.
146
+ const latestVersion = releases[0]?.version || CURRENT_VERSION;
143
147
  return {
144
148
  currentVersion: CURRENT_VERSION,
145
149
  latestVersion,
146
150
  updateAvailable: compareVersions(latestVersion, CURRENT_VERSION) > 0,
147
151
  checkedAt: new Date().toISOString(),
148
- releases: buildReleaseList(metadata),
152
+ releases,
149
153
  sourceUnavailable: false
150
154
  };
151
155
  }
@@ -215,7 +219,18 @@ router.post('/self-update', async (req, res) => {
215
219
 
216
220
  selfUpdateRunning = true;
217
221
  try {
218
- const version = normalizeUpdateVersion(req.body?.version);
222
+ let version = normalizeUpdateVersion(req.body?.version);
223
+ if (version === 'latest') {
224
+ const status = cachedResult && !cachedResult.sourceUnavailable && Date.now() - cachedAt < CACHE_TTL_MS
225
+ ? cachedResult
226
+ : await checkForUpdates();
227
+ version = status.latestVersion;
228
+ }
229
+
230
+ if (compareVersions(version, CURRENT_VERSION) <= 0) {
231
+ return res.json({ success: true, message: `DevBuddy is already up to date (${CURRENT_VERSION}).` });
232
+ }
233
+
219
234
  if (process.platform === 'win32') {
220
235
  scheduleWindowsSelfUpdate(`${PACKAGE_NAME}@${version}`);
221
236
  cachedResult = null;
@@ -1,7 +1,7 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
- import { join } from 'node:path';
4
+ import { dirname, join } from 'node:path';
5
5
  import { readSettings } from '../repositories/settings.js';
6
6
  import { openWindowsTerminal } from './windows.js';
7
7
 
@@ -109,6 +109,17 @@ export function openInSystemFolder(targetPath) {
109
109
  }
110
110
  }
111
111
 
112
+ export function revealInSystemFolder(targetPath) {
113
+ if (!targetPath || typeof targetPath !== 'string') return;
114
+ if (process.platform === 'darwin') {
115
+ execFile('open', ['-R', targetPath]);
116
+ } else if (process.platform === 'win32') {
117
+ execFile('explorer.exe', [`/select,${targetPath}`], { windowsHide: true });
118
+ } else {
119
+ openInSystemFolder(dirname(targetPath));
120
+ }
121
+ }
122
+
112
123
  function execFileAsync(command, args) {
113
124
  return new Promise((resolve, reject) => {
114
125
  execFile(command, args, (error) => {
@@ -6,6 +6,7 @@ import { promisify } from 'node:util';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import encodeMozjpeg, { init as initMozjpeg } from '@jsquash/jpeg/encode.js';
8
8
  import { paths } from '../config.js';
9
+ import { addErrorRecord } from '../repositories/errors.js';
9
10
  import { readSettings } from '../repositories/settings.js';
10
11
  import { readWindowsClipboard } from './windows.js';
11
12
 
@@ -15,6 +16,22 @@ let lastValue = '';
15
16
  let lastImageHash = '';
16
17
  let suppressImageCaptureUntil = 0;
17
18
  let mozjpegWasmModule = null;
19
+ const clipboardErrorLogTimes = new Map();
20
+
21
+ function logClipboardError(stage, error) {
22
+ // Clipboard polling runs continuously. Avoid filling Error Log with the
23
+ // same native-tool failure every two seconds while retaining diagnostics.
24
+ const now = Date.now();
25
+ const lastLoggedAt = clipboardErrorLogTimes.get(stage) || 0;
26
+ if (now - lastLoggedAt < 60 * 1000) return;
27
+ clipboardErrorLogTimes.set(stage, now);
28
+ const details = String(error?.stderr || error?.stdout || error?.stack || error?.message || error || '').trim();
29
+ addErrorRecord({
30
+ source: 'Clipboard Monitor',
31
+ message: `Failed during ${stage}.`,
32
+ details: details || 'The native clipboard operation returned an unknown error.'
33
+ });
34
+ }
18
35
 
19
36
  async function compressMozjpeg(rgbaBuf, width, height, quality = 75) {
20
37
  if (!mozjpegWasmModule) {
@@ -72,7 +89,8 @@ async function getMacClipboardImageData() {
72
89
  const base64 = str.slice(secondColon + 1);
73
90
  if (!width || !height || !base64) return null;
74
91
  return { width, height, rgbaBuf: Buffer.from(base64, 'base64') };
75
- } catch {
92
+ } catch (error) {
93
+ logClipboardError('macOS image clipboard read (osascript)', error);
76
94
  return null;
77
95
  }
78
96
  }
@@ -95,7 +113,8 @@ if let data = pb.data(forType: .png) {
95
113
  try {
96
114
  const { stdout } = await execFileAsync('swift', ['-e', swiftCode], { encoding: 'buffer', timeout: 3000 });
97
115
  if (stdout && stdout.length > 0) return stdout;
98
- } catch {
116
+ } catch (error) {
117
+ logClipboardError('macOS image clipboard read (Swift)', error);
99
118
  try {
100
119
  const jsaScript = `
101
120
  ObjC.import("AppKit");
@@ -118,7 +137,9 @@ if let data = pb.data(forType: .png) {
118
137
  const { stdout } = await execFileAsync('osascript', ['-l', 'JavaScript', '-e', jsaScript], { timeout: 3000 });
119
138
  const str = stdout.trim();
120
139
  if (str) return Buffer.from(str, 'base64');
121
- } catch {}
140
+ } catch (fallbackError) {
141
+ logClipboardError('macOS image clipboard fallback read (osascript)', fallbackError);
142
+ }
122
143
  }
123
144
  return null;
124
145
  }
@@ -330,9 +351,16 @@ export async function captureClipboard() {
330
351
  try {
331
352
  const mozjpegBuf = await compressMozjpeg(imageData.rgbaBuf, imageData.width, imageData.height, 75);
332
353
  saveClipboardImage(mozjpegBuf, 'jpg', hash);
333
- } catch {
354
+ } catch (error) {
355
+ logClipboardError('image compression or save (WASM/JPEG)', error);
334
356
  const rawBuf = await getMacClipboardImageBuffer();
335
- if (rawBuf) saveClipboardImage(rawBuf, 'png', hash);
357
+ if (rawBuf) {
358
+ try {
359
+ saveClipboardImage(rawBuf, 'png', hash);
360
+ } catch (fallbackError) {
361
+ logClipboardError('raw image save after compression fallback', fallbackError);
362
+ }
363
+ }
336
364
  }
337
365
  }
338
366
  }
@@ -357,7 +385,11 @@ export async function captureClipboard() {
357
385
 
358
386
  lastImageHash = hash;
359
387
  if (!isDuplicate) {
360
- saveClipboardImage(imageBuffer, 'png', hash);
388
+ try {
389
+ saveClipboardImage(imageBuffer, 'png', hash);
390
+ } catch (error) {
391
+ logClipboardError('image save', error);
392
+ }
361
393
  }
362
394
  }
363
395
  }