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.
|
|
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) =>
|
|
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
|
|
package/server/services/git.js
CHANGED
|
@@ -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
|
-
|
|
87
|
-
|
|
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 [];
|