buddy-workbench 0.1.14 → 0.1.15

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.15",
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",
@@ -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
 
@@ -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,6 +100,7 @@ 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
106
  if (process.platform === 'win32') return [];