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/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
- try {
20
- if (command && (command.includes("docker run") || command.includes("docker-compose up") || command.includes("docker compose up"))) {
21
- return await isDockerContainerRunning(command);
22
- }
23
- if (isWindows()) {
24
- const result = await $`tasklist /FI "PID eq ${pid}" /NH`.nothrow().text();
25
- return result.includes(`${pid}`);
26
- } else {
27
- const result = await $`ps -p ${pid}`.nothrow().text();
28
- return result.includes(`${pid}`);
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
- } catch {
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 $`wmic process where (ParentProcessId=${pid}) get ProcessId`.nothrow().text();
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
- const children = await getChildPids(pid);
70
- for (const childPid of children) {
71
- try {
72
- if (isWindows()) {
73
- if (force) {
74
- await $`taskkill /F /PID ${childPid}`.nothrow().quiet();
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
- await $`taskkill /PID ${childPid}`.nothrow().quiet();
84
+ const signal = force ? "KILL" : "TERM";
85
+ await $`kill -${signal} ${childPid}`.nothrow();
77
86
  }
78
- } else {
79
- const signal = force ? "KILL" : "TERM";
80
- await $`kill -${signal} ${childPid}`.nothrow();
81
- }
82
- } catch {}
83
- }
84
- await Bun.sleep(500);
85
- if (await isProcessRunning(pid)) {
86
- try {
87
- if (isWindows()) {
88
- if (force) {
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
- await $`taskkill /PID ${pid}`.nothrow().quiet();
99
+ const signal = force ? "KILL" : "TERM";
100
+ await $`kill -${signal} ${pid}`.nothrow();
92
101
  }
93
- } else {
94
- const signal = force ? "KILL" : "TERM";
95
- await $`kill -${signal} ${pid}`.nothrow();
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
- try {
204
- const content = await Bun.file(filePath).text();
205
- if (!lines) {
206
- return content;
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
- const allLines = content.split(/\r?\n/);
209
- const tailLines = allLines.slice(-lines);
210
- return tailLines.join(`
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
- } catch (error) {
213
- throw new Error(`Error reading file: ${error}`);
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 dbName = process.env.DB_NAME ?? "bgr";
358
- var dbPath = join(homePath, ".bgr", `${dbName}_v2.sqlite`);
359
- ensureDir(join(homePath, ".bgr"));
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
- try {
485
- await $2`git fetch origin`;
486
- const localHash = (await $2`git rev-parse HEAD`.text()).trim();
487
- const remoteHash = (await $2`git rev-parse origin/$(git rev-parse --abbrev-ref HEAD)`.text()).trim();
488
- if (localHash !== remoteHash) {
489
- await $2`git pull origin $(git rev-parse --abbrev-ref HEAD)`;
490
- announce("\uD83D\uDCE5 Pulled latest changes", "Git Update");
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
- } catch (err) {
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 terminateProcess(existingProcess.pid);
506
- announce(`\uD83D\uDD25 Terminated existing process '${name}'`, "Process Terminated");
507
- }
508
- for (const port of detectedPorts) {
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
- for (const port of detectedPorts) {
512
- const freed = await waitForPortFree(port, 5000);
513
- if (!freed) {
514
- await killProcessOnPort(port);
515
- await waitForPortFree(port, 3000);
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
- try {
541
- const newConfigEnv = await parseConfigFile(fullConfigPath);
542
- finalEnv = { ...finalEnv, ...newConfigEnv };
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 newProcess = Bun.spawn(getShellCommand(finalCommand), {
556
- env: { ...Bun.env, ...finalEnv },
557
- cwd: finalDirectory,
558
- stdout: Bun.file(stdoutPath),
559
- stderr: Bun.file(stderrPath)
560
- });
561
- newProcess.unref();
562
- await sleep2(100);
563
- const actualPid = await findChildPid(newProcess.pid);
564
- await sleep2(400);
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 run() {
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 bgrDir = join3(homePath3, ".bgr");
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(bgrDir, `${dashboardName}-out.txt`);
1320
- const stderrPath = join3(bgrDir, `${dashboardName}-err.txt`);
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: bgrDir,
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: bgrDir,
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
- run().catch((err) => {
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.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
- "melina": "^1.3.2",
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 { isProcessRunning, terminateProcess, readFileTail, getProcessPorts, findChildPid, findPidByPort, getShellCommand, killProcessOnPort, waitForPortFree, ensureDir, getHomeDir, isWindows } from './platform'
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'
@@ -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