bgrun 3.3.3 → 3.7.0
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/dashboard/app/api/config/[name]/route.ts +55 -0
- package/dashboard/app/api/debug/route.ts +18 -0
- package/dashboard/app/api/events/route.ts +60 -0
- package/dashboard/app/api/logs/[name]/route.ts +55 -5
- package/dashboard/app/api/processes/[name]/route.ts +5 -2
- package/dashboard/app/api/processes/route.ts +95 -35
- package/dashboard/app/api/restart/[name]/route.ts +4 -3
- package/dashboard/app/api/start/route.ts +4 -3
- package/dashboard/app/api/stop/[name]/route.ts +11 -4
- package/dashboard/app/api/version/route.ts +4 -2
- package/dashboard/app/globals.css +1010 -205
- package/dashboard/app/layout.tsx +2 -23
- package/dashboard/app/page.client.tsx +720 -58
- package/dashboard/app/page.tsx +100 -22
- package/dist/index.js +222 -96
- package/package.json +6 -3
- package/src/api.ts +17 -2
- package/src/commands/list.ts +15 -1
- package/src/commands/run.ts +60 -47
- package/src/db.ts +40 -3
- package/src/index.ts +27 -1
- package/src/platform.ts +225 -75
- package/src/table.ts +2 -0
package/dist/index.js
CHANGED
|
@@ -9,6 +9,8 @@ import { parseArgs } from "util";
|
|
|
9
9
|
import * as fs from "fs";
|
|
10
10
|
import * as os from "os";
|
|
11
11
|
var {$ } = globalThis.Bun;
|
|
12
|
+
import { createMeasure } from "measure-fn";
|
|
13
|
+
var plat = createMeasure("platform");
|
|
12
14
|
function isWindows() {
|
|
13
15
|
return process.platform === "win32";
|
|
14
16
|
}
|
|
@@ -16,20 +18,22 @@ function getHomeDir() {
|
|
|
16
18
|
return os.homedir();
|
|
17
19
|
}
|
|
18
20
|
async function isProcessRunning(pid, command) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
return plat.measure(`PID ${pid} alive?`, async () => {
|
|
22
|
+
try {
|
|
23
|
+
if (command && (command.includes("docker run") || command.includes("docker-compose up") || command.includes("docker compose up"))) {
|
|
24
|
+
return await isDockerContainerRunning(command);
|
|
25
|
+
}
|
|
26
|
+
if (isWindows()) {
|
|
27
|
+
const result = await $`tasklist /FI "PID eq ${pid}" /NH`.nothrow().text();
|
|
28
|
+
return result.includes(`${pid}`);
|
|
29
|
+
} else {
|
|
30
|
+
const result = await $`ps -p ${pid}`.nothrow().text();
|
|
31
|
+
return result.includes(`${pid}`);
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
29
35
|
}
|
|
30
|
-
}
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
36
|
+
});
|
|
33
37
|
}
|
|
34
38
|
async function isDockerContainerRunning(command) {
|
|
35
39
|
try {
|
|
@@ -53,7 +57,7 @@ async function isDockerContainerRunning(command) {
|
|
|
53
57
|
async function getChildPids(pid) {
|
|
54
58
|
try {
|
|
55
59
|
if (isWindows()) {
|
|
56
|
-
const result = await $`
|
|
60
|
+
const result = await $`powershell -Command "Get-CimInstance Win32_Process -Filter 'ParentProcessId=${pid}' | Select-Object -ExpandProperty ProcessId"`.nothrow().text();
|
|
57
61
|
return result.split(`
|
|
58
62
|
`).map((line) => parseInt(line.trim())).filter((n) => !isNaN(n) && n > 0);
|
|
59
63
|
} else {
|
|
@@ -66,36 +70,38 @@ async function getChildPids(pid) {
|
|
|
66
70
|
}
|
|
67
71
|
}
|
|
68
72
|
async function terminateProcess(pid, force = false) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
|
|
73
|
+
await plat.measure(`Terminate PID ${pid}`, async (m) => {
|
|
74
|
+
const children = await m("Get children", () => getChildPids(pid)) ?? [];
|
|
75
|
+
for (const childPid of children) {
|
|
76
|
+
try {
|
|
77
|
+
if (isWindows()) {
|
|
78
|
+
if (force) {
|
|
79
|
+
await $`taskkill /F /PID ${childPid}`.nothrow().quiet();
|
|
80
|
+
} else {
|
|
81
|
+
await $`taskkill /PID ${childPid}`.nothrow().quiet();
|
|
82
|
+
}
|
|
75
83
|
} else {
|
|
76
|
-
|
|
84
|
+
const signal = force ? "KILL" : "TERM";
|
|
85
|
+
await $`kill -${signal} ${childPid}`.nothrow();
|
|
77
86
|
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
await $`taskkill /F /PID ${pid}`.nothrow().quiet();
|
|
87
|
+
} catch {}
|
|
88
|
+
}
|
|
89
|
+
await Bun.sleep(500);
|
|
90
|
+
if (await isProcessRunning(pid)) {
|
|
91
|
+
try {
|
|
92
|
+
if (isWindows()) {
|
|
93
|
+
if (force) {
|
|
94
|
+
await $`taskkill /F /PID ${pid}`.nothrow().quiet();
|
|
95
|
+
} else {
|
|
96
|
+
await $`taskkill /PID ${pid}`.nothrow().quiet();
|
|
97
|
+
}
|
|
90
98
|
} else {
|
|
91
|
-
|
|
99
|
+
const signal = force ? "KILL" : "TERM";
|
|
100
|
+
await $`kill -${signal} ${pid}`.nothrow();
|
|
92
101
|
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
} catch {}
|
|
98
|
-
}
|
|
102
|
+
} catch {}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
99
105
|
}
|
|
100
106
|
async function isPortFree(port) {
|
|
101
107
|
try {
|
|
@@ -200,18 +206,65 @@ async function findChildPid(parentPid) {
|
|
|
200
206
|
return currentPid;
|
|
201
207
|
}
|
|
202
208
|
async function readFileTail(filePath, lines) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
209
|
+
return plat.measure(`Read tail ${lines ?? "all"}L`, async () => {
|
|
210
|
+
try {
|
|
211
|
+
const content = await Bun.file(filePath).text();
|
|
212
|
+
if (!lines) {
|
|
213
|
+
return content;
|
|
214
|
+
}
|
|
215
|
+
const allLines = content.split(/\r?\n/);
|
|
216
|
+
const tailLines = allLines.slice(-lines);
|
|
217
|
+
return tailLines.join(`
|
|
218
|
+
`);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
throw new Error(`Error reading file: ${error}`);
|
|
207
221
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
async function getProcessBatchMemory(pids) {
|
|
225
|
+
if (pids.length === 0)
|
|
226
|
+
return new Map;
|
|
227
|
+
return await plat.measure(`Batch memory (${pids.length} PIDs)`, async () => {
|
|
228
|
+
const memoryMap = new Map;
|
|
229
|
+
const pidSet = new Set(pids);
|
|
230
|
+
try {
|
|
231
|
+
if (isWindows()) {
|
|
232
|
+
const result = await $`powershell -Command "Get-Process | Select-Object Id, WorkingSet"`.nothrow().quiet().text();
|
|
233
|
+
const lines = result.trim().split(`
|
|
211
234
|
`);
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
235
|
+
for (const line of lines) {
|
|
236
|
+
const trimmed = line.trim();
|
|
237
|
+
if (!trimmed || trimmed.startsWith("Id") || trimmed.startsWith("--"))
|
|
238
|
+
continue;
|
|
239
|
+
const parts = trimmed.split(/\s+/);
|
|
240
|
+
if (parts.length >= 2) {
|
|
241
|
+
const val1 = parseInt(parts[0]);
|
|
242
|
+
const val2 = parseInt(parts[parts.length - 1]);
|
|
243
|
+
if (!isNaN(val1) && !isNaN(val2)) {
|
|
244
|
+
if (pidSet.has(val1))
|
|
245
|
+
memoryMap.set(val1, val2);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
const result = await $`ps -eo pid,rss`.nothrow().quiet().text();
|
|
251
|
+
const lines = result.trim().split(`
|
|
252
|
+
`);
|
|
253
|
+
for (let i = 1;i < lines.length; i++) {
|
|
254
|
+
const line = lines[i].trim();
|
|
255
|
+
if (!line)
|
|
256
|
+
continue;
|
|
257
|
+
const [pidStr, rssStr] = line.split(/\s+/);
|
|
258
|
+
const pid = parseInt(pidStr);
|
|
259
|
+
const rss = parseInt(rssStr);
|
|
260
|
+
if (pidSet.has(pid)) {
|
|
261
|
+
memoryMap.set(pid, rss * 1024);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} catch (e) {}
|
|
266
|
+
return memoryMap;
|
|
267
|
+
}) ?? new Map;
|
|
215
268
|
}
|
|
216
269
|
async function getProcessPorts(pid) {
|
|
217
270
|
try {
|
|
@@ -342,6 +395,7 @@ function tailFile(path, prefix, colorFn, lines) {
|
|
|
342
395
|
import { Database, z } from "sqlite-zod-orm";
|
|
343
396
|
import { join } from "path";
|
|
344
397
|
var {sleep } = globalThis.Bun;
|
|
398
|
+
import { existsSync as existsSync3, copyFileSync as copyFileSync2 } from "fs";
|
|
345
399
|
var ProcessSchema = z.object({
|
|
346
400
|
pid: z.number(),
|
|
347
401
|
workdir: z.string(),
|
|
@@ -354,9 +408,18 @@ var ProcessSchema = z.object({
|
|
|
354
408
|
timestamp: z.string().default(() => new Date().toISOString())
|
|
355
409
|
});
|
|
356
410
|
var homePath = getHomeDir();
|
|
357
|
-
var
|
|
358
|
-
|
|
359
|
-
|
|
411
|
+
var bgrDir = join(homePath, ".bgr");
|
|
412
|
+
ensureDir(bgrDir);
|
|
413
|
+
var dbFilename = process.env.BGRUN_DB ?? "bgrun.sqlite";
|
|
414
|
+
var dbPath = join(bgrDir, dbFilename);
|
|
415
|
+
var bgrHome = bgrDir;
|
|
416
|
+
var legacyDbPath = join(bgrDir, "bgr_v2.sqlite");
|
|
417
|
+
if (!existsSync3(dbPath) && existsSync3(legacyDbPath)) {
|
|
418
|
+
try {
|
|
419
|
+
copyFileSync2(legacyDbPath, dbPath);
|
|
420
|
+
console.log(`[bgrun] Migrated database: ${legacyDbPath} \u2192 ${dbPath}`);
|
|
421
|
+
} catch (e) {}
|
|
422
|
+
}
|
|
360
423
|
var db = new Database(dbPath, {
|
|
361
424
|
process: ProcessSchema
|
|
362
425
|
}, {
|
|
@@ -394,6 +457,14 @@ function removeAllProcesses() {
|
|
|
394
457
|
db.process.delete(p.id);
|
|
395
458
|
}
|
|
396
459
|
}
|
|
460
|
+
function getDbInfo() {
|
|
461
|
+
return {
|
|
462
|
+
dbPath,
|
|
463
|
+
bgrHome,
|
|
464
|
+
dbFilename,
|
|
465
|
+
exists: existsSync3(dbPath)
|
|
466
|
+
};
|
|
467
|
+
}
|
|
397
468
|
async function retryDatabaseOperation(operation, maxRetries = 5, delay = 100) {
|
|
398
469
|
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
399
470
|
try {
|
|
@@ -469,7 +540,9 @@ async function parseConfigFile(configPath) {
|
|
|
469
540
|
var {$: $2 } = globalThis.Bun;
|
|
470
541
|
var {sleep: sleep2 } = globalThis.Bun;
|
|
471
542
|
import { join as join2 } from "path";
|
|
543
|
+
import { createMeasure as createMeasure2 } from "measure-fn";
|
|
472
544
|
var homePath2 = getHomeDir();
|
|
545
|
+
var run = createMeasure2("run");
|
|
473
546
|
async function handleRun(options) {
|
|
474
547
|
const { command, directory, env, name, configPath, force, fetch, stdout, stderr } = options;
|
|
475
548
|
const existingProcess = name ? getProcess(name) : null;
|
|
@@ -481,17 +554,19 @@ async function handleRun(options) {
|
|
|
481
554
|
if (!__require("fs").existsSync(__require("path").join(finalDirectory2, ".git"))) {
|
|
482
555
|
error(`Cannot --fetch: '${finalDirectory2}' is not a Git repository.`);
|
|
483
556
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
557
|
+
await run.measure(`Git fetch "${name}"`, async () => {
|
|
558
|
+
try {
|
|
559
|
+
await $2`git fetch origin`;
|
|
560
|
+
const localHash = (await $2`git rev-parse HEAD`.text()).trim();
|
|
561
|
+
const remoteHash = (await $2`git rev-parse origin/$(git rev-parse --abbrev-ref HEAD)`.text()).trim();
|
|
562
|
+
if (localHash !== remoteHash) {
|
|
563
|
+
await $2`git pull origin $(git rev-parse --abbrev-ref HEAD)`;
|
|
564
|
+
announce("\uD83D\uDCE5 Pulled latest changes", "Git Update");
|
|
565
|
+
}
|
|
566
|
+
} catch (err) {
|
|
567
|
+
error(`Failed to pull latest changes: ${err}`);
|
|
491
568
|
}
|
|
492
|
-
}
|
|
493
|
-
error(`Failed to pull latest changes: ${err}`);
|
|
494
|
-
}
|
|
569
|
+
});
|
|
495
570
|
}
|
|
496
571
|
const isRunning = await isProcessRunning(existingProcess.pid);
|
|
497
572
|
if (isRunning && !force) {
|
|
@@ -502,18 +577,24 @@ async function handleRun(options) {
|
|
|
502
577
|
detectedPorts = await getProcessPorts(existingProcess.pid);
|
|
503
578
|
}
|
|
504
579
|
if (isRunning) {
|
|
505
|
-
await
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
await killProcessOnPort(port);
|
|
580
|
+
await run.measure(`Terminate "${name}" (PID ${existingProcess.pid})`, async () => {
|
|
581
|
+
await terminateProcess(existingProcess.pid);
|
|
582
|
+
announce(`\uD83D\uDD25 Terminated existing process '${name}'`, "Process Terminated");
|
|
583
|
+
});
|
|
510
584
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
585
|
+
if (detectedPorts.length > 0) {
|
|
586
|
+
await run.measure(`Port cleanup [${detectedPorts.join(", ")}]`, async () => {
|
|
587
|
+
for (const port of detectedPorts) {
|
|
588
|
+
await killProcessOnPort(port);
|
|
589
|
+
}
|
|
590
|
+
for (const port of detectedPorts) {
|
|
591
|
+
const freed = await waitForPortFree(port, 5000);
|
|
592
|
+
if (!freed) {
|
|
593
|
+
await killProcessOnPort(port);
|
|
594
|
+
await waitForPortFree(port, 3000);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
});
|
|
517
598
|
}
|
|
518
599
|
await retryDatabaseOperation(() => removeProcessByName(name));
|
|
519
600
|
} else {
|
|
@@ -537,12 +618,17 @@ async function handleRun(options) {
|
|
|
537
618
|
if (finalConfigPath) {
|
|
538
619
|
const fullConfigPath = join2(finalDirectory, finalConfigPath);
|
|
539
620
|
if (await Bun.file(fullConfigPath).exists()) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
621
|
+
const configEnv = await run.measure(`Parse config "${finalConfigPath}"`, async () => {
|
|
622
|
+
try {
|
|
623
|
+
return await parseConfigFile(fullConfigPath);
|
|
624
|
+
} catch (err) {
|
|
625
|
+
console.warn(`Warning: Failed to parse config file ${finalConfigPath}: ${err.message}`);
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
if (configEnv) {
|
|
630
|
+
finalEnv = { ...finalEnv, ...configEnv };
|
|
543
631
|
console.log(`Loaded config from ${finalConfigPath}`);
|
|
544
|
-
} catch (err) {
|
|
545
|
-
console.warn(`Warning: Failed to parse config file ${finalConfigPath}: ${err.message}`);
|
|
546
632
|
}
|
|
547
633
|
} else {
|
|
548
634
|
console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
|
|
@@ -552,16 +638,19 @@ async function handleRun(options) {
|
|
|
552
638
|
Bun.write(stdoutPath, "");
|
|
553
639
|
const stderrPath = stderr || existingProcess?.stderr_path || join2(homePath2, ".bgr", `${name}-err.txt`);
|
|
554
640
|
Bun.write(stderrPath, "");
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
641
|
+
const actualPid = await run.measure(`Spawn "${name}" \u2192 ${finalCommand}`, async () => {
|
|
642
|
+
const newProcess = Bun.spawn(getShellCommand(finalCommand), {
|
|
643
|
+
env: { ...Bun.env, ...finalEnv },
|
|
644
|
+
cwd: finalDirectory,
|
|
645
|
+
stdout: Bun.file(stdoutPath),
|
|
646
|
+
stderr: Bun.file(stderrPath)
|
|
647
|
+
});
|
|
648
|
+
newProcess.unref();
|
|
649
|
+
await sleep2(100);
|
|
650
|
+
const pid = await findChildPid(newProcess.pid);
|
|
651
|
+
await sleep2(400);
|
|
652
|
+
return pid;
|
|
653
|
+
}) ?? 0;
|
|
565
654
|
await retryDatabaseOperation(() => insertProcess({
|
|
566
655
|
pid: actualPid,
|
|
567
656
|
workdir: finalDirectory,
|
|
@@ -732,6 +821,7 @@ function renderProcessTable(processes, options) {
|
|
|
732
821
|
{ key: "pid", header: "PID", formatter: (pid) => chalk3.yellow(pid) },
|
|
733
822
|
{ key: "name", header: "Name", formatter: (name) => chalk3.cyan.bold(name) },
|
|
734
823
|
{ key: "port", header: "Port", formatter: (port) => port === "-" ? chalk3.gray(port) : chalk3.hex("#FF6B6B")(port) },
|
|
824
|
+
{ key: "memory", header: "Memory", formatter: (mem) => mem === "-" ? chalk3.gray(mem) : chalk3.hex("#4ECDC4")(mem) },
|
|
735
825
|
{ key: "command", header: "Command" },
|
|
736
826
|
{ key: "workdir", header: "Directory", formatter: (dir) => chalk3.gray(dir), truncator: truncatePath },
|
|
737
827
|
{ key: "status", header: "Status" },
|
|
@@ -741,6 +831,14 @@ function renderProcessTable(processes, options) {
|
|
|
741
831
|
}
|
|
742
832
|
|
|
743
833
|
// src/commands/list.ts
|
|
834
|
+
function formatMemory(bytes) {
|
|
835
|
+
if (bytes === 0)
|
|
836
|
+
return "-";
|
|
837
|
+
const mb = bytes / (1024 * 1024);
|
|
838
|
+
if (mb >= 1024)
|
|
839
|
+
return `${(mb / 1024).toFixed(1)} GB`;
|
|
840
|
+
return `${Math.round(mb)} MB`;
|
|
841
|
+
}
|
|
744
842
|
async function showAll(opts) {
|
|
745
843
|
const processes = getAllProcesses();
|
|
746
844
|
const filtered = processes.filter((proc) => {
|
|
@@ -767,15 +865,19 @@ async function showAll(opts) {
|
|
|
767
865
|
return;
|
|
768
866
|
}
|
|
769
867
|
const tableData = [];
|
|
868
|
+
const allPids = filtered.map((p) => p.pid);
|
|
869
|
+
const memoryMap = await getProcessBatchMemory(allPids);
|
|
770
870
|
for (const proc of filtered) {
|
|
771
871
|
const isRunning = await isProcessRunning(proc.pid, proc.command);
|
|
772
872
|
const runtime = calculateRuntime(proc.timestamp);
|
|
873
|
+
const mem = isRunning ? memoryMap.get(proc.pid) || 0 : 0;
|
|
773
874
|
const ports = isRunning ? await getProcessPorts(proc.pid) : [];
|
|
774
875
|
tableData.push({
|
|
775
876
|
id: proc.id,
|
|
776
877
|
pid: proc.pid,
|
|
777
878
|
name: proc.name,
|
|
778
879
|
port: ports.length > 0 ? ports.map((p) => `:${p}`).join(",") : "-",
|
|
880
|
+
memory: formatMemory(mem),
|
|
779
881
|
command: proc.command,
|
|
780
882
|
workdir: proc.workdir,
|
|
781
883
|
status: isRunning ? chalk4.green.bold("\u25CF Running") : chalk4.red.bold("\u25CB Stopped"),
|
|
@@ -1202,6 +1304,12 @@ import dedent from "dedent";
|
|
|
1202
1304
|
import chalk8 from "chalk";
|
|
1203
1305
|
import { join as join3 } from "path";
|
|
1204
1306
|
var {sleep: sleep3 } = globalThis.Bun;
|
|
1307
|
+
import { configure } from "measure-fn";
|
|
1308
|
+
if (!Bun.argv.includes("--_serve")) {
|
|
1309
|
+
if (!Bun.env.MEASURE_SILENT) {
|
|
1310
|
+
configure({ silent: true });
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1205
1313
|
async function showHelp() {
|
|
1206
1314
|
const usage = dedent`
|
|
1207
1315
|
${chalk8.bold("bgrun \u2014 Bun Background Runner")}
|
|
@@ -1235,6 +1343,7 @@ async function showHelp() {
|
|
|
1235
1343
|
--log-stderr Show only stderr logs
|
|
1236
1344
|
--lines <n> Number of log lines to show (default: all)
|
|
1237
1345
|
--version Show version
|
|
1346
|
+
--debug Show debug info (DB path, BGR home, etc.)
|
|
1238
1347
|
--dashboard Launch web dashboard as bgrun-managed process
|
|
1239
1348
|
--port <number> Port for dashboard (default: 3000)
|
|
1240
1349
|
--help Show this help message
|
|
@@ -1246,7 +1355,7 @@ async function showHelp() {
|
|
|
1246
1355
|
`;
|
|
1247
1356
|
console.log(usage);
|
|
1248
1357
|
}
|
|
1249
|
-
async function
|
|
1358
|
+
async function run2() {
|
|
1250
1359
|
const { values, positionals } = parseArgs({
|
|
1251
1360
|
args: Bun.argv.slice(2),
|
|
1252
1361
|
options: {
|
|
@@ -1274,6 +1383,7 @@ async function run() {
|
|
|
1274
1383
|
stdout: { type: "string" },
|
|
1275
1384
|
stderr: { type: "string" },
|
|
1276
1385
|
dashboard: { type: "boolean" },
|
|
1386
|
+
debug: { type: "boolean" },
|
|
1277
1387
|
_serve: { type: "boolean" },
|
|
1278
1388
|
port: { type: "string" }
|
|
1279
1389
|
},
|
|
@@ -1287,7 +1397,7 @@ async function run() {
|
|
|
1287
1397
|
if (values.dashboard) {
|
|
1288
1398
|
const dashboardName = "bgr-dashboard";
|
|
1289
1399
|
const homePath3 = getHomeDir();
|
|
1290
|
-
const
|
|
1400
|
+
const bgrDir2 = join3(homePath3, ".bgr");
|
|
1291
1401
|
const requestedPort = values.port;
|
|
1292
1402
|
const existing = getProcess(dashboardName);
|
|
1293
1403
|
if (existing && await isProcessRunning(existing.pid)) {
|
|
@@ -1316,8 +1426,8 @@ async function run() {
|
|
|
1316
1426
|
const scriptPath = resolve(process.argv[1]);
|
|
1317
1427
|
const spawnCommand = `bun run ${scriptPath} --_serve`;
|
|
1318
1428
|
const command = `bgrun --_serve`;
|
|
1319
|
-
const stdoutPath = join3(
|
|
1320
|
-
const stderrPath = join3(
|
|
1429
|
+
const stdoutPath = join3(bgrDir2, `${dashboardName}-out.txt`);
|
|
1430
|
+
const stderrPath = join3(bgrDir2, `${dashboardName}-err.txt`);
|
|
1321
1431
|
await Bun.write(stdoutPath, "");
|
|
1322
1432
|
await Bun.write(stderrPath, "");
|
|
1323
1433
|
const spawnEnv = { ...Bun.env };
|
|
@@ -1326,7 +1436,7 @@ async function run() {
|
|
|
1326
1436
|
}
|
|
1327
1437
|
const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
|
|
1328
1438
|
env: spawnEnv,
|
|
1329
|
-
cwd:
|
|
1439
|
+
cwd: bgrDir2,
|
|
1330
1440
|
stdout: Bun.file(stdoutPath),
|
|
1331
1441
|
stderr: Bun.file(stderrPath)
|
|
1332
1442
|
});
|
|
@@ -1344,7 +1454,7 @@ async function run() {
|
|
|
1344
1454
|
}
|
|
1345
1455
|
await retryDatabaseOperation(() => insertProcess({
|
|
1346
1456
|
pid: actualPid,
|
|
1347
|
-
workdir:
|
|
1457
|
+
workdir: bgrDir2,
|
|
1348
1458
|
command,
|
|
1349
1459
|
name: dashboardName,
|
|
1350
1460
|
env: "",
|
|
@@ -1380,6 +1490,22 @@ async function run() {
|
|
|
1380
1490
|
await showHelp();
|
|
1381
1491
|
return;
|
|
1382
1492
|
}
|
|
1493
|
+
if (values.debug) {
|
|
1494
|
+
const info = getDbInfo();
|
|
1495
|
+
const version = await getVersion();
|
|
1496
|
+
console.log(dedent`
|
|
1497
|
+
${chalk8.bold("bgrun debug info")}
|
|
1498
|
+
${chalk8.gray("\u2500".repeat(40))}
|
|
1499
|
+
Version: ${chalk8.cyan(version)}
|
|
1500
|
+
BGR Home: ${chalk8.yellow(info.bgrHome)}
|
|
1501
|
+
DB Path: ${chalk8.yellow(info.dbPath)}
|
|
1502
|
+
DB File: ${info.dbFilename}
|
|
1503
|
+
DB Exists: ${info.exists ? chalk8.green("\u2713") : chalk8.red("\u2717")}
|
|
1504
|
+
Platform: ${process.platform}
|
|
1505
|
+
Bun: ${Bun.version}
|
|
1506
|
+
`);
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1383
1509
|
if (values.nuke) {
|
|
1384
1510
|
await handleDeleteAll();
|
|
1385
1511
|
return;
|
|
@@ -1472,7 +1598,7 @@ async function run() {
|
|
|
1472
1598
|
});
|
|
1473
1599
|
}
|
|
1474
1600
|
}
|
|
1475
|
-
|
|
1601
|
+
run2().catch((err) => {
|
|
1476
1602
|
console.error(chalk8.red(err));
|
|
1477
1603
|
process.exit(1);
|
|
1478
1604
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bgrun",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
4
4
|
"description": "bgrun — A lightweight process manager for Bun",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/api.ts",
|
|
@@ -47,10 +47,13 @@
|
|
|
47
47
|
"boxen": "^8.0.1",
|
|
48
48
|
"chalk": "^5.4.1",
|
|
49
49
|
"dedent": "^1.5.3",
|
|
50
|
-
"
|
|
50
|
+
"measure-fn": "^3.2.1",
|
|
51
|
+
"melina": "^2.2.1",
|
|
52
|
+
"react": "^19.2.4",
|
|
53
|
+
"react-dom": "^19.2.4",
|
|
51
54
|
"sqlite-zod-orm": "^3.8.0"
|
|
52
55
|
},
|
|
53
56
|
"engines": {
|
|
54
57
|
"bun": ">=1.0.0"
|
|
55
58
|
}
|
|
56
|
-
}
|
|
59
|
+
}
|
package/src/api.ts
CHANGED
|
@@ -23,10 +23,25 @@ export type { Process } from './db'
|
|
|
23
23
|
export type { CommandOptions } from './types'
|
|
24
24
|
|
|
25
25
|
// --- Database Operations ---
|
|
26
|
-
export { db, getAllProcesses, getProcess, insertProcess, removeProcess, removeProcessByName, removeAllProcesses, retryDatabaseOperation } from './db'
|
|
26
|
+
export { db, getAllProcesses, getProcess, insertProcess, removeProcess, removeProcessByName, removeAllProcesses, retryDatabaseOperation, getDbInfo, dbPath, bgrHome } from './db'
|
|
27
27
|
|
|
28
28
|
// --- Process Operations ---
|
|
29
|
-
export {
|
|
29
|
+
export {
|
|
30
|
+
isProcessRunning,
|
|
31
|
+
terminateProcess,
|
|
32
|
+
readFileTail,
|
|
33
|
+
getProcessPorts,
|
|
34
|
+
findChildPid,
|
|
35
|
+
findPidByPort,
|
|
36
|
+
getShellCommand,
|
|
37
|
+
killProcessOnPort,
|
|
38
|
+
waitForPortFree,
|
|
39
|
+
ensureDir,
|
|
40
|
+
getHomeDir,
|
|
41
|
+
isWindows,
|
|
42
|
+
getProcessBatchMemory,
|
|
43
|
+
getProcessMemory
|
|
44
|
+
} from './platform'
|
|
30
45
|
|
|
31
46
|
// --- High-Level Commands ---
|
|
32
47
|
export { handleRun } from './commands/run'
|
package/src/commands/list.ts
CHANGED
|
@@ -4,7 +4,15 @@ import type { ProcessTableRow } from "../table";
|
|
|
4
4
|
import { getAllProcesses } from "../db";
|
|
5
5
|
import { announce } from "../logger";
|
|
6
6
|
import { isProcessRunning, calculateRuntime, parseEnvString } from "../utils";
|
|
7
|
-
import { getProcessPorts } from "../platform";
|
|
7
|
+
import { getProcessPorts, getProcessBatchMemory } from "../platform";
|
|
8
|
+
import { measure } from "measure-fn";
|
|
9
|
+
|
|
10
|
+
function formatMemory(bytes: number): string {
|
|
11
|
+
if (bytes === 0) return '-';
|
|
12
|
+
const mb = bytes / (1024 * 1024);
|
|
13
|
+
if (mb >= 1024) return `${(mb / 1024).toFixed(1)} GB`;
|
|
14
|
+
return `${Math.round(mb)} MB`;
|
|
15
|
+
}
|
|
8
16
|
|
|
9
17
|
export async function showAll(opts?: { json?: boolean; filter?: string }) {
|
|
10
18
|
const processes = getAllProcesses();
|
|
@@ -41,9 +49,14 @@ export async function showAll(opts?: { json?: boolean; filter?: string }) {
|
|
|
41
49
|
// Table output
|
|
42
50
|
const tableData: ProcessTableRow[] = [];
|
|
43
51
|
|
|
52
|
+
// Batch fetch memory for all PIDs
|
|
53
|
+
const allPids = filtered.map(p => p.pid);
|
|
54
|
+
const memoryMap = await getProcessBatchMemory(allPids);
|
|
55
|
+
|
|
44
56
|
for (const proc of filtered) {
|
|
45
57
|
const isRunning = await isProcessRunning(proc.pid, proc.command);
|
|
46
58
|
const runtime = calculateRuntime(proc.timestamp);
|
|
59
|
+
const mem = isRunning ? (memoryMap.get(proc.pid) || 0) : 0;
|
|
47
60
|
|
|
48
61
|
const ports = isRunning ? await getProcessPorts(proc.pid) : [];
|
|
49
62
|
tableData.push({
|
|
@@ -51,6 +64,7 @@ export async function showAll(opts?: { json?: boolean; filter?: string }) {
|
|
|
51
64
|
pid: proc.pid,
|
|
52
65
|
name: proc.name,
|
|
53
66
|
port: ports.length > 0 ? ports.map(p => `:${p}`).join(',') : '-',
|
|
67
|
+
memory: formatMemory(mem),
|
|
54
68
|
command: proc.command,
|
|
55
69
|
workdir: proc.workdir,
|
|
56
70
|
status: isRunning
|