buddy-workbench 0.1.14 → 0.1.16

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.14",
3
+ "version": "0.1.16",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,8 @@
18
18
  "dev": "node --watch server.js",
19
19
  "build": "npm --prefix ui run build",
20
20
  "dev:ui": "npm --prefix ui run dev",
21
- "prepublishOnly": "npm run build"
21
+ "prepublishOnly": "npm run build",
22
+ "version": "node -e \"const v=process.env.npm_package_version; const fs=require('fs'); ['ui/package.json', 'ui/package-lock.json'].forEach(p=>{if(fs.existsSync(p)){const j=JSON.parse(fs.readFileSync(p)); j.version=v; if(j.packages&&j.packages['']){j.packages[''].version=v;} fs.writeFileSync(p, JSON.stringify(j,null,2)+'\\n');}});\" && git add ui/package.json ui/package-lock.json"
22
23
  },
23
24
  "dependencies": {
24
25
  "axios": "^1.7.9",
@@ -53,7 +53,7 @@ export function saveDefaultEditor(defaultEditor) {
53
53
 
54
54
  export function saveDefaultBrowser(defaultBrowser) {
55
55
  const settings = readSettings();
56
- const valid = ['chrome', 'edge', 'safari'];
56
+ const valid = process.platform === 'win32' ? ['chrome', 'edge'] : ['chrome', 'edge', 'safari'];
57
57
  settings.defaultBrowser = valid.includes(defaultBrowser) ? defaultBrowser : 'chrome';
58
58
  mkdirSync(dirname(paths.settings), { recursive: true });
59
59
  writeFileSync(paths.settings, JSON.stringify(settings, null, 2), { mode: 0o600 });
@@ -1,5 +1,5 @@
1
1
  import express, { Router } from 'express';
2
- import { clipboardDates, clipboardItems, clipboardOriginal, deleteClipboardItem, saveClipboardImage, clipboardImagePath, allTaggedClipboardItems, updateClipboardItemTags } from '../services/clipboard-history.js';
2
+ import { clipboardDates, clipboardItems, clipboardOriginal, deleteClipboardItem, saveClipboardImage, clipboardImagePath, allTaggedClipboardItems, updateClipboardItemTags, markClipboardCopied } from '../services/clipboard-history.js';
3
3
 
4
4
  const router = Router();
5
5
  router.get('/', (req, res) => res.json({ dates: clipboardDates(), items: clipboardItems(req.query.date) }));
@@ -36,5 +36,14 @@ router.put('/:date/:id/tags', (req, res) => {
36
36
  if (!updated) return res.status(404).json({ error: 'Clipboard entry not found.' });
37
37
  res.json(updated);
38
38
  });
39
+ router.post('/:date/:id/copied', async (req, res) => {
40
+ try {
41
+ const updated = await markClipboardCopied(req.params.date, req.params.id);
42
+ res.json(updated || { ok: true });
43
+ } catch (error) {
44
+ res.status(500).json({ error: error.message });
45
+ }
46
+ });
39
47
  router.delete('/:date/:id', (req, res) => { if (!deleteClipboardItem(req.params.date, req.params.id)) return res.status(404).json({ error: 'Clipboard entry not found.' }); res.status(204).end(); });
40
48
  export default router;
49
+
@@ -1,8 +1,8 @@
1
1
  import { Router } from 'express';
2
2
  import { listLaunchers, saveLaunchers } from '../repositories/launchers.js';
3
- import { runScript, runningErrorCounts, runningScripts, scriptErrorLogs, scriptLogs, stopScript } from '../services/process-manager.js';
3
+ import { clearScriptLogs, runScript, runningErrorCounts, runningScripts, scriptErrorLogs, scriptLogs, stopScript } from '../services/process-manager.js';
4
4
  import { readPackageScripts } from '../services/package-scripts.js';
5
- import { currentGitBranch } from '../services/git.js';
5
+ import { currentGitBranch, getGitRemoteUrl, toWebRepoUrl } from '../services/git.js';
6
6
 
7
7
  const router = Router();
8
8
  function validLauncher(body) {
@@ -17,7 +17,18 @@ const invalid = (res) => res.status(400).json({ error: 'Name, project folder, an
17
17
 
18
18
  router.get('/', async (_req, res) => {
19
19
  const launchers = listLaunchers();
20
- const items = await Promise.all(launchers.map(async (launcher) => ({ ...launcher, packageScriptCount: readPackageScripts(launcher.folder).length, branch: await currentGitBranch(launcher.folder) })));
20
+ const items = await Promise.all(launchers.map(async (launcher) => {
21
+ const [branch, remoteUrl] = await Promise.all([
22
+ currentGitBranch(launcher.folder),
23
+ getGitRemoteUrl(launcher.folder)
24
+ ]);
25
+ return {
26
+ ...launcher,
27
+ packageScriptCount: readPackageScripts(launcher.folder).length,
28
+ branch,
29
+ repoUrl: toWebRepoUrl(remoteUrl)
30
+ };
31
+ }));
21
32
  res.json(items);
22
33
  });
23
34
  router.post('/', (req, res) => { const config = validLauncher(req.body); if (!config) return invalid(res); const launcher = { id: crypto.randomUUID(), ...config }; const launchers = listLaunchers(); saveLaunchers([...launchers, launcher]); res.status(201).json(launcher); });
@@ -39,8 +50,10 @@ router.post('/:id/install/run', (req, res) => { const launcher = listLaunchers()
39
50
  router.post('/:id/package-scripts/:scriptName/run', (req, res) => { const launcher = listLaunchers().find((item) => item.id === req.params.id); if (!launcher) return res.status(404).json({ error: 'Configuration not found.' }); const script = readPackageScripts(launcher.folder).find((item) => item.name === req.params.scriptName); if (!script) return res.status(404).json({ error: 'Package script not found.' }); try { runScript(launcher, { ...script, command: `${launcher.executor} run ${script.name}` }); res.json({ ok: true }); } catch (error) { res.status(error.message.includes('already') ? 409 : 400).json({ error: error.message }); } });
40
51
  router.get('/:id/package-scripts/:scriptName/logs', (req, res) => { const script = readPackageScripts(listLaunchers().find((item) => item.id === req.params.id)?.folder || '').find((item) => item.name === req.params.scriptName); if (!script) return res.status(404).json({ error: 'Package script not found.' }); res.json({ log: scriptLogs(req.params.id, script.id) }); });
41
52
  router.get('/:id/package-scripts/:scriptName/logs/error', (req, res) => { const script = readPackageScripts(listLaunchers().find((item) => item.id === req.params.id)?.folder || '').find((item) => item.name === req.params.scriptName); if (!script) return res.status(404).json({ error: 'Package script not found.' }); res.json({ log: scriptErrorLogs(req.params.id, script.id) }); });
53
+ router.delete('/:id/package-scripts/:scriptName/logs', (req, res) => { const script = readPackageScripts(listLaunchers().find((item) => item.id === req.params.id)?.folder || '').find((item) => item.name === req.params.scriptName); if (!script) return res.status(404).json({ error: 'Package script not found.' }); clearScriptLogs(req.params.id, script.id); res.json({ ok: true }); });
42
54
  router.get('/:id/scripts/:scriptId/logs', (req, res) => res.json({ log: scriptLogs(req.params.id, req.params.scriptId) }));
43
55
  router.get('/:id/scripts/:scriptId/logs/error', (req, res) => res.json({ log: scriptErrorLogs(req.params.id, req.params.scriptId) }));
56
+ router.delete('/:id/scripts/:scriptId/logs', (req, res) => { clearScriptLogs(req.params.id, req.params.scriptId); res.json({ ok: true }); });
44
57
  router.post('/:id/scripts/:scriptId/run', (req, res) => { const launcher = listLaunchers().find((item) => item.id === req.params.id); if (!launcher) return res.status(404).json({ error: 'Configuration not found.' }); const script = launcher.scripts.find((item) => item.id === req.params.scriptId); if (!script) return res.status(404).json({ error: 'Script not found.' }); try { runScript(launcher, script); res.json({ ok: true }); } catch (error) { res.status(error.message.includes('already') ? 409 : 400).json({ error: error.message }); } });
45
58
  router.post('/:id/scripts/:scriptId/stop', (req, res) => { try { stopScript(req.params.id, req.params.scriptId); res.json({ ok: true }); } catch (error) { res.status(404).json({ error: error.message }); } });
46
59
 
@@ -33,10 +33,17 @@ export function getBrowserBookmarkPaths() {
33
33
  * Checks if a browser process is currently running.
34
34
  */
35
35
  export function checkBrowserRunning(browserName) {
36
+ const platform = os.platform();
36
37
  try {
37
- const processPattern = browserName === 'chrome' ? 'Google Chrome' : 'Microsoft Edge';
38
- const output = execSync(`pgrep -f "${processPattern}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
39
- return output.trim().length > 0;
38
+ if (platform === 'win32') {
39
+ const exeName = browserName === 'chrome' ? 'chrome.exe' : 'msedge.exe';
40
+ const output = execSync(`tasklist /FI "IMAGENAME eq ${exeName}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
41
+ return output.toLowerCase().includes(exeName.toLowerCase());
42
+ } else {
43
+ const processPattern = browserName === 'chrome' ? 'Google Chrome' : 'Microsoft Edge';
44
+ const output = execSync(`pgrep -f "${processPattern}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
45
+ return output.trim().length > 0;
46
+ }
40
47
  } catch {
41
48
  return false;
42
49
  }
@@ -287,7 +287,11 @@ export async function captureClipboard() {
287
287
  const dates = clipboardDates();
288
288
  for (const d of dates) {
289
289
  const dayList = dayItems(d);
290
- if (dayList.some(item => item.imageHash === hash)) {
290
+ if (dayList.some(item => {
291
+ if (item.imageHash === hash || item.fileHash === hash) return true;
292
+ if (Array.isArray(item.imageHashes) && item.imageHashes.includes(hash)) return true;
293
+ return false;
294
+ })) {
291
295
  isDuplicate = true;
292
296
  break;
293
297
  }
@@ -313,7 +317,11 @@ export async function captureClipboard() {
313
317
  const dates = clipboardDates();
314
318
  for (const d of dates) {
315
319
  const dayList = dayItems(d);
316
- if (dayList.some(item => item.imageHash === hash)) {
320
+ if (dayList.some(item => {
321
+ if (item.imageHash === hash || item.fileHash === hash) return true;
322
+ if (Array.isArray(item.imageHashes) && item.imageHashes.includes(hash)) return true;
323
+ return false;
324
+ })) {
317
325
  isDuplicate = true;
318
326
  break;
319
327
  }
@@ -332,11 +340,61 @@ export async function captureClipboard() {
332
340
 
333
341
  export function startClipboardCapture() { captureClipboard(); const timer = setInterval(captureClipboard, 2000); timer.unref(); }
334
342
 
343
+ export async function markClipboardCopied(date, id) {
344
+ let targetDate = date;
345
+ let items = dayItems(targetDate);
346
+ let item = items.find((entry) => entry.id === id);
347
+
348
+ if (!item) {
349
+ const dates = clipboardDates();
350
+ for (const d of dates) {
351
+ const dayList = dayItems(d);
352
+ const found = dayList.find((entry) => entry.id === id);
353
+ if (found) {
354
+ targetDate = d;
355
+ items = dayList;
356
+ item = found;
357
+ break;
358
+ }
359
+ }
360
+ }
361
+
362
+ if (!item) return null;
363
+
364
+ if (item.imageFile) {
365
+ try {
366
+ const imageData = await getMacClipboardImageData();
367
+ if (imageData && imageData.rgbaBuf.length > 0) {
368
+ const currentHash = createHash('md5').update(imageData.rgbaBuf).digest('hex');
369
+ lastImageHash = currentHash;
370
+
371
+ const existingHashes = Array.isArray(item.imageHashes)
372
+ ? item.imageHashes
373
+ : [item.imageHash, item.fileHash].filter(Boolean);
374
+
375
+ if (!existingHashes.includes(currentHash)) {
376
+ const updatedItem = { ...item, imageHashes: [...existingHashes, currentHash] };
377
+ const nextItems = items.map((entry) => (entry.id === id ? updatedItem : entry));
378
+ writeJson(dayFile(targetDate), nextItems);
379
+ return updatedItem;
380
+ }
381
+ }
382
+ } catch {}
383
+ } else {
384
+ const text = clipboardOriginal(targetDate, id) || item.text || item.preview || '';
385
+ if (text) {
386
+ lastValue = text.trim();
387
+ }
388
+ }
389
+ return item;
390
+ }
391
+
335
392
  export function saveClipboardImage(buffer, ext = 'png', imageHash = '') {
336
393
  const date = today();
337
394
  const id = randomUUID();
338
395
  const filename = `${id}.${ext}`;
339
- const hash = imageHash || createHash('md5').update(buffer).digest('hex');
396
+ const fileHash = createHash('md5').update(buffer).digest('hex');
397
+ const hash = imageHash || fileHash;
340
398
 
341
399
  mkdirSync(join(paths.clipboardDir, 'content'), { recursive: true });
342
400
  writeFileSync(join(paths.clipboardDir, 'content', filename), buffer);
@@ -345,6 +403,8 @@ export function saveClipboardImage(buffer, ext = 'png', imageHash = '') {
345
403
  id,
346
404
  imageFile: filename,
347
405
  imageHash: hash,
406
+ fileHash,
407
+ imageHashes: Array.from(new Set([hash, fileHash].filter(Boolean))),
348
408
  createdAt: new Date().toISOString()
349
409
  };
350
410
 
@@ -12,3 +12,79 @@ export async function currentGitBranch(folder) {
12
12
  return null;
13
13
  }
14
14
  }
15
+
16
+ export async function getGitRemoteUrl(folder) {
17
+ if (!folder) return null;
18
+ try {
19
+ const { stdout } = await execFileAsync('git', ['config', '--get', 'remote.origin.url'], { cwd: folder, timeout: 1500 });
20
+ const url = stdout.trim();
21
+ if (url) return url;
22
+ } catch {}
23
+ try {
24
+ const { stdout } = await execFileAsync('git', ['remote', '-v'], { cwd: folder, timeout: 1500 });
25
+ const match = stdout.match(/\S+\s+(\S+)\s+\(fetch\)/);
26
+ if (match) return match[1].trim();
27
+ } catch {}
28
+ return null;
29
+ }
30
+
31
+ export function toWebRepoUrl(remoteUrl) {
32
+ if (!remoteUrl || typeof remoteUrl !== 'string') return null;
33
+ let raw = remoteUrl.trim();
34
+ if (!raw) return null;
35
+
36
+ // Remove trailing .git
37
+ raw = raw.replace(/\.git$/i, '');
38
+
39
+ // Handle SCP-like SSH syntax: git@host:owner/repo
40
+ const scpMatch = raw.match(/^([a-zA-Z0-9_.-]+@)?([^:]+):(.+)$/);
41
+ if (!raw.includes('://') && scpMatch) {
42
+ const host = scpMatch[2];
43
+ let path = scpMatch[3].replace(/^\/+/, '');
44
+
45
+ if (host.includes('bitbucket') && !host.includes('bitbucket.org')) {
46
+ const parts = path.split('/');
47
+ if (parts.length === 2 && parts[0] !== 'projects' && parts[0] !== 'users') {
48
+ const projKey = parts[0].startsWith('~') ? `users/${parts[0].slice(1)}` : `projects/${parts[0]}`;
49
+ path = `${projKey}/repos/${parts[1]}`;
50
+ } else if (parts.length === 3 && parts[0] === 'scm') {
51
+ const projKey = parts[1].startsWith('~') ? `users/${parts[1].slice(1)}` : `projects/${parts[1]}`;
52
+ path = `${projKey}/repos/${parts[2]}`;
53
+ }
54
+ }
55
+ return `https://${host}/${path}`;
56
+ }
57
+
58
+ // Handle URLs with protocols (ssh://, http://, https://)
59
+ try {
60
+ let urlStr = raw;
61
+ const isSshProto = urlStr.startsWith('ssh://');
62
+ if (isSshProto) {
63
+ urlStr = urlStr.replace(/^ssh:\/\//i, 'https://');
64
+ }
65
+ // Strip userinfo (e.g. git@ or user:pass@)
66
+ urlStr = urlStr.replace(/^(https?:\/\/)(([^/@]+)@)/i, '$1');
67
+
68
+ const urlObj = new URL(urlStr);
69
+ const host = urlObj.hostname;
70
+ let pathname = urlObj.pathname.replace(/^\/+/, '');
71
+
72
+ if (host.includes('bitbucket') && !host.includes('bitbucket.org')) {
73
+ const parts = pathname.split('/');
74
+ if (parts.length === 2 && parts[0] !== 'projects' && parts[0] !== 'users') {
75
+ const projKey = parts[0].startsWith('~') ? `users/${parts[0].slice(1)}` : `projects/${parts[0]}`;
76
+ pathname = `${projKey}/repos/${parts[1]}`;
77
+ } else if (parts.length === 3 && parts[0] === 'scm') {
78
+ const projKey = parts[1].startsWith('~') ? `users/${parts[1].slice(1)}` : `projects/${parts[1]}`;
79
+ pathname = `${projKey}/repos/${parts[2]}`;
80
+ }
81
+ }
82
+
83
+ const protocol = raw.startsWith('http://') ? 'http:' : 'https:';
84
+ const portStr = (urlObj.port && !isSshProto) ? `:${urlObj.port}` : '';
85
+ return `${protocol}//${host}${portStr}/${pathname}`;
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
@@ -60,7 +60,6 @@ export function countErrorBlocks(text, isStderr = false) {
60
60
  }
61
61
 
62
62
  const isErrorLine =
63
- isStderr ||
64
63
  isNewHeader ||
65
64
  /\b(error|failed|fatal|exception)\b/i.test(cleanLine) ||
66
65
  /^npm ERR!/i.test(cleanLine) ||
@@ -83,8 +82,11 @@ export function countErrorBlocks(text, isStderr = false) {
83
82
  const appendLog = (key, type, chunk) => {
84
83
  const current = logs.get(key) || { output: '', error: '', errorCount: 0 };
85
84
  const str = String(chunk || '');
86
- current[type] = `${current[type]}${str}`.split(/\r?\n/).slice(-1000).join('\n');
87
- current.errorCount = countErrorBlocks(current.error, true) + countErrorBlocks(current.output, false);
85
+ if (type === 'error' || type === 'stderr') {
86
+ current.error = `${current.error}${str}`.split(/\r?\n/).slice(-1000).join('\n');
87
+ }
88
+ current.output = `${current.output}${str}`.split(/\r?\n/).slice(-1000).join('\n');
89
+ current.errorCount = countErrorBlocks(current.output, false);
88
90
  logs.set(key, current);
89
91
  };
90
92
 
@@ -98,9 +100,26 @@ export function runningErrorCounts() {
98
100
  }
99
101
  export function scriptLogs(launcherId, scriptId) { return logs.get(keyFor(launcherId, scriptId))?.output || ''; }
100
102
  export function scriptErrorLogs(launcherId, scriptId) { return logs.get(keyFor(launcherId, scriptId))?.error || ''; }
103
+ export function clearScriptLogs(launcherId, scriptId) { logs.set(keyFor(launcherId, scriptId), { output: '', error: '', errorCount: 0 }); }
101
104
 
102
105
  async function listeningProcesses() {
103
- if (process.platform === 'win32') return [];
106
+ if (process.platform === 'win32') {
107
+ try {
108
+ const { stdout } = await execFileAsync('netstat', ['-ano', '-p', 'tcp']);
109
+ const rows = [];
110
+ const matches = stdout.matchAll(/TCP\s+(?:\[::\]|[\d.]+):(\d+)\s+.*?LISTENING\s+(\d+)/gi);
111
+ for (const m of matches) {
112
+ const port = Number(m[1]);
113
+ const pid = Number(m[2]);
114
+ if (port && pid) {
115
+ rows.push({ pid, port, command: 'node' });
116
+ }
117
+ }
118
+ return rows;
119
+ } catch {
120
+ return [];
121
+ }
122
+ }
104
123
  try {
105
124
  const { stdout } = await execFileAsync('lsof', ['-nP', '-iTCP', '-sTCP:LISTEN', '-F', 'pcn'], { maxBuffer: 1024 * 1024 });
106
125
  const rows = [];