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 +3 -2
- package/server/repositories/settings.js +1 -1
- package/server/routes/clipboard.js +10 -1
- package/server/routes/launchers.js +16 -3
- package/server/services/bookmark-sync.js +10 -3
- package/server/services/clipboard-history.js +63 -3
- package/server/services/git.js +76 -0
- package/server/services/process-manager.js +23 -4
- package/ui/dist/assets/index-C8IWgbWG.js +524 -0
- package/ui/dist/index.html +1 -1
- package/ui/dist/assets/index-MhpHbNrk.js +0 -524
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "buddy-workbench",
|
|
3
|
-
"version": "0.1.
|
|
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) =>
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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 =>
|
|
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 =>
|
|
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
|
|
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
|
|
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,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')
|
|
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 = [];
|