bgrun 3.10.1 → 3.11.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
@@ -27,7 +27,7 @@ function getHomeDir() {
27
27
  async function isProcessRunning(pid, command) {
28
28
  if (pid <= 0)
29
29
  return false;
30
- return plat.measure(`PID ${pid} alive?`, async () => {
30
+ return await plat.measure(`PID ${pid} alive?`, async () => {
31
31
  try {
32
32
  if (command && (command.includes("docker run") || command.includes("docker-compose up") || command.includes("docker compose up"))) {
33
33
  return await isDockerContainerRunning(command);
@@ -42,7 +42,7 @@ async function isProcessRunning(pid, command) {
42
42
  } catch {
43
43
  return false;
44
44
  }
45
- });
45
+ }) ?? false;
46
46
  }
47
47
  async function isDockerContainerRunning(command) {
48
48
  try {
@@ -155,6 +155,8 @@ async function killProcessOnPort(port) {
155
155
  if (alive) {
156
156
  await $`taskkill /F /T /PID ${pid}`.nothrow().quiet();
157
157
  console.log(`Killed process ${pid} using port ${port}`);
158
+ } else {
159
+ console.warn(`\u26A0 Port ${port} held by zombie PID ${pid} (process dead, socket stuck in kernel). Will clear on reboot or TCP timeout.`);
158
160
  }
159
161
  }
160
162
  } else {
@@ -210,7 +212,7 @@ async function findChildPid(parentPid) {
210
212
  return currentPid;
211
213
  }
212
214
  async function readFileTail(filePath, lines) {
213
- return plat.measure(`Read tail ${lines ?? "all"}L`, async () => {
215
+ return await plat.measure(`Read tail ${lines ?? "all"}L`, async () => {
214
216
  try {
215
217
  const content = await Bun.file(filePath).text();
216
218
  if (!lines) {
@@ -223,17 +225,17 @@ async function readFileTail(filePath, lines) {
223
225
  } catch (error) {
224
226
  throw new Error(`Error reading file: ${error}`);
225
227
  }
226
- });
228
+ }) ?? "";
227
229
  }
228
- async function getProcessBatchMemory(pids) {
230
+ async function getProcessBatchResources(pids) {
229
231
  if (pids.length === 0)
230
232
  return new Map;
231
- return await plat.measure(`Batch memory (${pids.length} PIDs)`, async () => {
232
- const memoryMap = new Map;
233
+ return await plat.measure(`Batch resources (${pids.length} PIDs)`, async () => {
234
+ const resourceMap = new Map;
233
235
  const pidSet = new Set(pids);
234
236
  try {
235
237
  if (isWindows()) {
236
- const result = await $`powershell -Command "Get-Process | Select-Object Id, WorkingSet"`.nothrow().quiet().text();
238
+ const result = await $`powershell -Command "Get-Process | Select-Object Id, CPU, WorkingSet"`.nothrow().quiet().text();
237
239
  const lines = result.trim().split(`
238
240
  `);
239
241
  for (const line of lines) {
@@ -241,33 +243,41 @@ async function getProcessBatchMemory(pids) {
241
243
  if (!trimmed || trimmed.startsWith("Id") || trimmed.startsWith("--"))
242
244
  continue;
243
245
  const parts = trimmed.split(/\s+/);
244
- if (parts.length >= 2) {
245
- const val1 = parseInt(parts[0]);
246
- const val2 = parseInt(parts[parts.length - 1]);
247
- if (!isNaN(val1) && !isNaN(val2)) {
248
- if (pidSet.has(val1))
249
- memoryMap.set(val1, val2);
246
+ if (parts.length >= 3) {
247
+ const pid = parseInt(parts[0]);
248
+ let cpuStr = parts[1];
249
+ let memStr = parts[2];
250
+ if (parts.length === 2) {
251
+ cpuStr = "0";
252
+ memStr = parts[1];
253
+ }
254
+ const cpu = parseFloat(cpuStr) || 0;
255
+ const memory = parseInt(memStr) || 0;
256
+ if (!isNaN(pid) && !isNaN(memory)) {
257
+ if (pidSet.has(pid))
258
+ resourceMap.set(pid, { memory, cpu });
250
259
  }
251
260
  }
252
261
  }
253
262
  } else {
254
- const result = await $`ps -eo pid,rss`.nothrow().quiet().text();
263
+ const result = await $`ps -eo pid,pcpu,rss`.nothrow().quiet().text();
255
264
  const lines = result.trim().split(`
256
265
  `);
257
266
  for (let i = 1;i < lines.length; i++) {
258
267
  const line = lines[i].trim();
259
268
  if (!line)
260
269
  continue;
261
- const [pidStr, rssStr] = line.split(/\s+/);
270
+ const [pidStr, cpuStr, rssStr] = line.split(/\s+/);
262
271
  const pid = parseInt(pidStr);
263
- const rss = parseInt(rssStr);
272
+ const cpu = parseFloat(cpuStr) || 0;
273
+ const rss = parseInt(rssStr) || 0;
264
274
  if (pidSet.has(pid)) {
265
- memoryMap.set(pid, rss * 1024);
275
+ resourceMap.set(pid, { memory: rss * 1024, cpu });
266
276
  }
267
277
  }
268
278
  }
269
279
  } catch (e) {}
270
- return memoryMap;
280
+ return resourceMap;
271
281
  }) ?? new Map;
272
282
  }
273
283
  async function getProcessPorts(pid) {
@@ -406,6 +416,7 @@ var init_utils = __esm(() => {
406
416
  var exports_db = {};
407
417
  __export(exports_db, {
408
418
  updateProcessPid: () => updateProcessPid,
419
+ updateProcessEnv: () => updateProcessEnv,
409
420
  retryDatabaseOperation: () => retryDatabaseOperation,
410
421
  removeProcessByName: () => removeProcessByName,
411
422
  removeProcess: () => removeProcess,
@@ -459,6 +470,12 @@ function removeAllProcesses() {
459
470
  db.process.delete(p.id);
460
471
  }
461
472
  }
473
+ function updateProcessEnv(name, envJson) {
474
+ const proc = db.process.select().where({ name }).limit(1).get();
475
+ if (proc) {
476
+ db.process.update(proc.id, { env: envJson });
477
+ }
478
+ }
462
479
  function getDbInfo() {
463
480
  return {
464
481
  dbPath,
@@ -521,7 +538,7 @@ var init_db = __esm(() => {
521
538
  import boxen from "boxen";
522
539
  import chalk2 from "chalk";
523
540
  function announce(message, title) {
524
- console.log(boxen(chalk2.white(message), {
541
+ console.log(boxen(message, {
525
542
  padding: 1,
526
543
  margin: 1,
527
544
  borderColor: "green",
@@ -531,7 +548,8 @@ function announce(message, title) {
531
548
  }));
532
549
  }
533
550
  function error(message) {
534
- console.error(boxen(chalk2.red(message), {
551
+ const text = message instanceof Error ? message.stack || message.message : String(message);
552
+ console.error(boxen(chalk2.red(text), {
535
553
  padding: 1,
536
554
  margin: 1,
537
555
  borderColor: "red",
@@ -574,6 +592,95 @@ async function parseConfigFile(configPath) {
574
592
  return flattenConfig(parsedConfig);
575
593
  }
576
594
 
595
+ // src/deps.ts
596
+ var exports_deps = {};
597
+ __export(exports_deps, {
598
+ getUnmetDeps: () => getUnmetDeps,
599
+ getDependencies: () => getDependencies,
600
+ buildDepGraph: () => buildDepGraph
601
+ });
602
+ function getDependencies(envStr) {
603
+ const env = parseEnvString(envStr);
604
+ const raw = env.BGR_DEPENDS_ON || "";
605
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
606
+ }
607
+ async function buildDepGraph() {
608
+ const processes = getAllProcesses();
609
+ const nodeMap = new Map;
610
+ for (const proc of processes) {
611
+ const deps = getDependencies(proc.env);
612
+ const alive = await isProcessRunning(proc.pid, proc.command);
613
+ nodeMap.set(proc.name, {
614
+ name: proc.name,
615
+ dependsOn: deps,
616
+ dependedBy: [],
617
+ running: alive,
618
+ pid: proc.pid
619
+ });
620
+ }
621
+ for (const node of nodeMap.values()) {
622
+ for (const dep of node.dependsOn) {
623
+ const depNode = nodeMap.get(dep);
624
+ if (depNode) {
625
+ depNode.dependedBy.push(node.name);
626
+ }
627
+ }
628
+ }
629
+ const inDegree = new Map;
630
+ for (const node of nodeMap.values()) {
631
+ inDegree.set(node.name, node.dependsOn.filter((d) => nodeMap.has(d)).length);
632
+ }
633
+ const queue = [];
634
+ for (const [name, degree] of inDegree) {
635
+ if (degree === 0)
636
+ queue.push(name);
637
+ }
638
+ const order = [];
639
+ while (queue.length > 0) {
640
+ const current = queue.shift();
641
+ order.push(current);
642
+ const node = nodeMap.get(current);
643
+ for (const dependent of node.dependedBy) {
644
+ const newDegree = (inDegree.get(dependent) || 0) - 1;
645
+ inDegree.set(dependent, newDegree);
646
+ if (newDegree === 0)
647
+ queue.push(dependent);
648
+ }
649
+ }
650
+ const hasCycle = order.length < nodeMap.size;
651
+ const cycleNodes = hasCycle ? [...nodeMap.keys()].filter((n) => !order.includes(n)) : undefined;
652
+ return {
653
+ nodes: [...nodeMap.values()],
654
+ order,
655
+ hasCycle,
656
+ cycleNodes
657
+ };
658
+ }
659
+ async function getUnmetDeps(name) {
660
+ const proc = getProcess(name);
661
+ if (!proc)
662
+ return [];
663
+ const deps = getDependencies(proc.env);
664
+ const unmet = [];
665
+ for (const depName of deps) {
666
+ const depProc = getProcess(depName);
667
+ if (!depProc) {
668
+ unmet.push(depName);
669
+ continue;
670
+ }
671
+ const alive = await isProcessRunning(depProc.pid, depProc.command);
672
+ if (!alive) {
673
+ unmet.push(depName);
674
+ }
675
+ }
676
+ return unmet;
677
+ }
678
+ var init_deps = __esm(() => {
679
+ init_db();
680
+ init_platform();
681
+ init_utils();
682
+ });
683
+
577
684
  // src/commands/run.ts
578
685
  var {$: $2 } = globalThis.Bun;
579
686
  var {sleep: sleep2 } = globalThis.Bun;
@@ -582,6 +689,21 @@ import { createMeasure as createMeasure2 } from "measure-fn";
582
689
  async function handleRun(options) {
583
690
  const { command, directory, env, name, configPath, force, fetch, stdout, stderr } = options;
584
691
  const existingProcess = name ? getProcess(name) : null;
692
+ if (name && existingProcess) {
693
+ const { getUnmetDeps: getUnmetDeps2 } = await Promise.resolve().then(() => (init_deps(), exports_deps));
694
+ const unmet = await getUnmetDeps2(name);
695
+ if (unmet.length > 0) {
696
+ await run.measure(`Start ${unmet.length} dependencies for "${name}"`, async () => {
697
+ for (const depName of unmet) {
698
+ const depProc = getProcess(depName);
699
+ if (depProc) {
700
+ announce(`\uD83D\uDCE6 Starting dependency "${depName}" for "${name}"`, "Dependency");
701
+ await handleRun({ action: "run", name: depName, force: true, remoteName: "" });
702
+ }
703
+ }
704
+ });
705
+ }
706
+ }
585
707
  if (existingProcess) {
586
708
  const finalDirectory2 = directory || existingProcess.workdir;
587
709
  validateDirectory(finalDirectory2);
@@ -725,10 +847,82 @@ var init_run = __esm(() => {
725
847
  run = createMeasure2("run");
726
848
  });
727
849
 
850
+ // src/log-rotation.ts
851
+ var exports_log_rotation = {};
852
+ __export(exports_log_rotation, {
853
+ startLogRotation: () => startLogRotation,
854
+ rotateLogFile: () => rotateLogFile,
855
+ rotateAllLogs: () => rotateAllLogs
856
+ });
857
+ import { existsSync as existsSync7, statSync as statSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
858
+ function rotateLogFile(filePath, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
859
+ try {
860
+ if (!existsSync7(filePath))
861
+ return false;
862
+ const stat = statSync3(filePath);
863
+ if (stat.size <= maxBytes)
864
+ return false;
865
+ const content = readFileSync2(filePath, "utf-8");
866
+ const lines = content.split(`
867
+ `);
868
+ if (lines.length <= keepLines)
869
+ return false;
870
+ const truncated = lines.slice(-keepLines);
871
+ const header = `--- [bgrun] Log rotated at ${new Date().toISOString()} (was ${lines.length} lines, ${formatBytes(stat.size)}) ---
872
+ `;
873
+ writeFileSync(filePath, header + truncated.join(`
874
+ `));
875
+ return true;
876
+ } catch {
877
+ return false;
878
+ }
879
+ }
880
+ function rotateAllLogs(getProcesses, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
881
+ const processes = getProcesses();
882
+ const rotated = [];
883
+ let checked = 0;
884
+ for (const proc of processes) {
885
+ if (proc.stdout_path) {
886
+ checked++;
887
+ if (rotateLogFile(proc.stdout_path, maxBytes, keepLines)) {
888
+ rotated.push(`${proc.name}/stdout`);
889
+ }
890
+ }
891
+ if (proc.stderr_path) {
892
+ checked++;
893
+ if (rotateLogFile(proc.stderr_path, maxBytes, keepLines)) {
894
+ rotated.push(`${proc.name}/stderr`);
895
+ }
896
+ }
897
+ }
898
+ return { rotated, checked };
899
+ }
900
+ function startLogRotation(getProcesses, intervalMs = DEFAULT_CHECK_INTERVAL_MS, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
901
+ console.log(`[logs] Log rotation active: max ${formatBytes(maxBytes)}/file, keep ${keepLines} lines, check every ${intervalMs / 1000}s`);
902
+ return setInterval(() => {
903
+ const { rotated } = rotateAllLogs(getProcesses, maxBytes, keepLines);
904
+ if (rotated.length > 0) {
905
+ console.log(`[logs] Rotated ${rotated.length} log(s): ${rotated.join(", ")}`);
906
+ }
907
+ }, intervalMs);
908
+ }
909
+ function formatBytes(bytes) {
910
+ if (bytes >= 1e6)
911
+ return `${(bytes / 1e6).toFixed(1)}MB`;
912
+ if (bytes >= 1000)
913
+ return `${(bytes / 1000).toFixed(0)}KB`;
914
+ return `${bytes}B`;
915
+ }
916
+ var DEFAULT_MAX_BYTES, DEFAULT_KEEP_LINES = 5000, DEFAULT_CHECK_INTERVAL_MS = 60000;
917
+ var init_log_rotation = __esm(() => {
918
+ DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
919
+ });
920
+
728
921
  // src/server.ts
729
922
  var exports_server = {};
730
923
  __export(exports_server, {
731
- startServer: () => startServer
924
+ startServer: () => startServer,
925
+ guardRestartCounts: () => guardRestartCounts
732
926
  });
733
927
  import path2 from "path";
734
928
  async function startServer() {
@@ -742,6 +936,8 @@ async function startServer() {
742
936
  ...explicitPort !== undefined && { port: explicitPort }
743
937
  });
744
938
  startGuard();
939
+ const { startLogRotation: startLogRotation2 } = await Promise.resolve().then(() => (init_log_rotation(), exports_log_rotation));
940
+ startLogRotation2(() => getAllProcesses());
745
941
  }
746
942
  function startGuard() {
747
943
  console.log(`[guard] \u2713 Built-in process guard started (checking every ${GUARD_INTERVAL_MS / 1000}s)`);
@@ -753,9 +949,16 @@ function startGuard() {
753
949
  for (const proc of processes) {
754
950
  if (GUARD_SKIP_NAMES.has(proc.name))
755
951
  continue;
952
+ const env = proc.env ? parseEnvString(proc.env) : {};
953
+ if (env.BGR_KEEP_ALIVE !== "true")
954
+ continue;
756
955
  const alive = await isProcessRunning(proc.pid, proc.command);
757
956
  if (!alive) {
758
- console.log(`[guard] \u26A0 Process "${proc.name}" (PID ${proc.pid}) is dead, restarting...`);
957
+ const now = Date.now();
958
+ const nextRestart = guardNextRestartTime.get(proc.name) || 0;
959
+ if (now < nextRestart)
960
+ continue;
961
+ console.log(`[guard] \u26A0 Guarded process "${proc.name}" (PID ${proc.pid}) is dead, restarting...`);
759
962
  try {
760
963
  await handleRun({
761
964
  action: "run",
@@ -763,10 +966,28 @@ function startGuard() {
763
966
  force: true,
764
967
  remoteName: ""
765
968
  });
766
- console.log(`[guard] \u2713 Restarted "${proc.name}"`);
969
+ const prevCount = guardRestartCounts.get(proc.name) || 0;
970
+ const newCount = prevCount + 1;
971
+ guardRestartCounts.set(proc.name, newCount);
972
+ if (newCount > 5) {
973
+ const backoffSeconds = Math.min(30 * Math.pow(2, newCount - 6), 300);
974
+ guardNextRestartTime.set(proc.name, Date.now() + backoffSeconds * 1000);
975
+ console.log(`[guard] \u2713 Restarted "${proc.name}" (restart #${newCount}). Crash loop detected: next check delayed by ${backoffSeconds}s.`);
976
+ } else {
977
+ console.log(`[guard] \u2713 Restarted "${proc.name}" (restart #${newCount})`);
978
+ }
767
979
  } catch (err) {
768
980
  console.error(`[guard] \u2717 Failed to restart "${proc.name}": ${err.message}`);
769
981
  }
982
+ } else {
983
+ const prevCount = guardRestartCounts.get(proc.name) || 0;
984
+ if (prevCount > 0) {
985
+ const nextRestart = guardNextRestartTime.get(proc.name) || 0;
986
+ if (Date.now() > nextRestart + 60000) {
987
+ guardRestartCounts.delete(proc.name);
988
+ guardNextRestartTime.delete(proc.name);
989
+ }
990
+ }
770
991
  }
771
992
  }
772
993
  } catch (err) {
@@ -774,12 +995,139 @@ function startGuard() {
774
995
  }
775
996
  }, GUARD_INTERVAL_MS);
776
997
  }
777
- var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES;
998
+ var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES, _g, guardRestartCounts, guardNextRestartTime;
778
999
  var init_server = __esm(() => {
779
1000
  init_db();
780
1001
  init_platform();
781
1002
  init_run();
782
- GUARD_SKIP_NAMES = new Set(["bgr-dashboard"]);
1003
+ init_utils();
1004
+ GUARD_SKIP_NAMES = new Set(["bgr-dashboard", "bgr-guard"]);
1005
+ _g = globalThis;
1006
+ if (!_g.__bgrGuardRestartCounts)
1007
+ _g.__bgrGuardRestartCounts = new Map;
1008
+ if (!_g.__bgrGuardNextRestartTime)
1009
+ _g.__bgrGuardNextRestartTime = new Map;
1010
+ guardRestartCounts = _g.__bgrGuardRestartCounts;
1011
+ guardNextRestartTime = _g.__bgrGuardNextRestartTime;
1012
+ });
1013
+
1014
+ // src/guard.ts
1015
+ var exports_guard = {};
1016
+ __export(exports_guard, {
1017
+ startGuardLoop: () => startGuardLoop
1018
+ });
1019
+ async function restartProcess(name) {
1020
+ try {
1021
+ await handleRun({
1022
+ action: "run",
1023
+ name,
1024
+ force: true,
1025
+ remoteName: ""
1026
+ });
1027
+ return true;
1028
+ } catch (err) {
1029
+ console.error(`[guard] \u2717 Failed to restart "${name}": ${err.message}`);
1030
+ return false;
1031
+ }
1032
+ }
1033
+ function getBackoffMs(restartCount) {
1034
+ if (restartCount <= CRASH_THRESHOLD)
1035
+ return 0;
1036
+ const exponent = restartCount - CRASH_THRESHOLD;
1037
+ return Math.min(30000 * Math.pow(2, exponent - 1), MAX_BACKOFF_MS);
1038
+ }
1039
+ async function guardCycle() {
1040
+ try {
1041
+ const processes = getAllProcesses();
1042
+ if (processes.length === 0)
1043
+ return;
1044
+ const now = Date.now();
1045
+ let checked = 0;
1046
+ let restarted = 0;
1047
+ let skipped = 0;
1048
+ for (const proc of processes) {
1049
+ if (proc.name === "bgr-guard")
1050
+ continue;
1051
+ const env = proc.env ? parseEnvString(proc.env) : {};
1052
+ const isGuarded = env.BGR_KEEP_ALIVE === "true";
1053
+ const isDashboard = proc.name === "bgr-dashboard";
1054
+ if (!isGuarded && !isDashboard)
1055
+ continue;
1056
+ checked++;
1057
+ try {
1058
+ const alive = await isProcessRunning(proc.pid, proc.command);
1059
+ if (!alive && proc.pid > 0) {
1060
+ const nextRestart = state.nextRestartTime.get(proc.name) || 0;
1061
+ if (now < nextRestart) {
1062
+ const waitSecs = Math.round((nextRestart - now) / 1000);
1063
+ skipped++;
1064
+ continue;
1065
+ }
1066
+ console.log(`[guard] \u26A0 "${proc.name}" (PID ${proc.pid}) is dead \u2014 restarting...`);
1067
+ const success = await restartProcess(proc.name);
1068
+ if (success) {
1069
+ const count = (state.restartCounts.get(proc.name) || 0) + 1;
1070
+ state.restartCounts.set(proc.name, count);
1071
+ state.lastSeenAlive.delete(proc.name);
1072
+ const backoff = getBackoffMs(count);
1073
+ if (backoff > 0) {
1074
+ state.nextRestartTime.set(proc.name, now + backoff);
1075
+ console.log(`[guard] \u2713 Restarted "${proc.name}" (#${count}). Crash loop: next check in ${Math.round(backoff / 1000)}s`);
1076
+ } else {
1077
+ console.log(`[guard] \u2713 Restarted "${proc.name}" (#${count})`);
1078
+ }
1079
+ restarted++;
1080
+ }
1081
+ } else if (alive) {
1082
+ const count = state.restartCounts.get(proc.name) || 0;
1083
+ if (count > 0) {
1084
+ const lastSeen = state.lastSeenAlive.get(proc.name);
1085
+ if (!lastSeen) {
1086
+ state.lastSeenAlive.set(proc.name, now);
1087
+ } else if (now - lastSeen > STABILITY_WINDOW_MS) {
1088
+ state.restartCounts.delete(proc.name);
1089
+ state.nextRestartTime.delete(proc.name);
1090
+ state.lastSeenAlive.delete(proc.name);
1091
+ console.log(`[guard] \u2713 "${proc.name}" stable for ${Math.round(STABILITY_WINDOW_MS / 1000)}s \u2014 reset counters`);
1092
+ }
1093
+ }
1094
+ }
1095
+ } catch (err) {
1096
+ console.error(`[guard] Error checking "${proc.name}": ${err.message}`);
1097
+ }
1098
+ }
1099
+ if (restarted > 0) {
1100
+ console.log(`[guard] Cycle: ${checked} checked, ${restarted} restarted, ${skipped} in backoff`);
1101
+ }
1102
+ } catch (err) {
1103
+ console.error(`[guard] Error in guard cycle: ${err.message}`);
1104
+ }
1105
+ }
1106
+ async function startGuardLoop(intervalMs = DEFAULT_INTERVAL_MS) {
1107
+ const interval = intervalMs || DEFAULT_INTERVAL_MS;
1108
+ console.log(`[guard] \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`);
1109
+ console.log(`[guard] \uD83D\uDEE1\uFE0F BGR Standalone Guard started`);
1110
+ console.log(`[guard] Check interval: ${interval / 1000}s`);
1111
+ console.log(`[guard] Crash backoff threshold: ${CRASH_THRESHOLD} restarts`);
1112
+ console.log(`[guard] Stability window: ${STABILITY_WINDOW_MS / 1000}s`);
1113
+ console.log(`[guard] Monitoring: BGR_KEEP_ALIVE=true + bgr-dashboard`);
1114
+ console.log(`[guard] Started: ${new Date().toLocaleString()}`);
1115
+ console.log(`[guard] \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`);
1116
+ await guardCycle();
1117
+ setInterval(guardCycle, interval);
1118
+ }
1119
+ var DEFAULT_INTERVAL_MS = 30000, MAX_BACKOFF_MS, CRASH_THRESHOLD = 5, STABILITY_WINDOW_MS = 120000, state;
1120
+ var init_guard = __esm(() => {
1121
+ init_db();
1122
+ init_platform();
1123
+ init_run();
1124
+ init_utils();
1125
+ MAX_BACKOFF_MS = 5 * 60000;
1126
+ state = {
1127
+ restartCounts: new Map,
1128
+ nextRestartTime: new Map,
1129
+ lastSeenAlive: new Map
1130
+ };
783
1131
  });
784
1132
 
785
1133
  // src/index.ts
@@ -993,11 +1341,11 @@ async function showAll(opts) {
993
1341
  }
994
1342
  const tableData = [];
995
1343
  const allPids = filtered.map((p) => p.pid);
996
- const memoryMap = await getProcessBatchMemory(allPids);
1344
+ const resourceMap = await getProcessBatchResources(allPids);
997
1345
  for (const proc of filtered) {
998
1346
  const isRunning = await isProcessRunning(proc.pid, proc.command);
999
1347
  const runtime = calculateRuntime(proc.timestamp);
1000
- const mem = isRunning ? memoryMap.get(proc.pid) || 0 : 0;
1348
+ const mem = isRunning ? resourceMap.get(proc.pid)?.memory || 0 : 0;
1001
1349
  const ports = isRunning ? await getProcessPorts(proc.pid) : [];
1002
1350
  tableData.push({
1003
1351
  id: proc.id,
@@ -1454,6 +1802,7 @@ async function showHelp() {
1454
1802
  bgrun List all processes
1455
1803
  bgrun [name] Show details for a process
1456
1804
  bgrun --dashboard Launch web dashboard (managed by bgrun)
1805
+ bgrun --guard Launch standalone process guard
1457
1806
  bgrun --restart [name] Restart a process
1458
1807
  bgrun --restart-all Restart ALL registered processes
1459
1808
  bgrun --stop [name] Stop a process (keep in registry)
@@ -1519,8 +1868,10 @@ async function run2() {
1519
1868
  stdout: { type: "string" },
1520
1869
  stderr: { type: "string" },
1521
1870
  dashboard: { type: "boolean" },
1871
+ guard: { type: "boolean" },
1522
1872
  debug: { type: "boolean" },
1523
1873
  _serve: { type: "boolean" },
1874
+ "_guard-loop": { type: "boolean" },
1524
1875
  port: { type: "string" }
1525
1876
  },
1526
1877
  strict: false,
@@ -1531,6 +1882,13 @@ async function run2() {
1531
1882
  await startServer2();
1532
1883
  return;
1533
1884
  }
1885
+ if (values["_guard-loop"]) {
1886
+ const { startGuardLoop: startGuardLoop2 } = await Promise.resolve().then(() => (init_guard(), exports_guard));
1887
+ const intervalStr = positionals[0];
1888
+ const intervalMs = intervalStr ? parseInt(intervalStr) * 1000 : undefined;
1889
+ await startGuardLoop2(intervalMs);
1890
+ return;
1891
+ }
1534
1892
  if (values.dashboard) {
1535
1893
  const dashboardName = "bgr-dashboard";
1536
1894
  const homePath3 = getHomeDir();
@@ -1577,6 +1935,18 @@ async function run2() {
1577
1935
  if (requestedPort) {
1578
1936
  spawnEnv.BUN_PORT = requestedPort;
1579
1937
  }
1938
+ const targetPort = parseInt(requestedPort || Bun.env.BUN_PORT || "3000");
1939
+ if (!isNaN(targetPort) && targetPort > 0) {
1940
+ const portFree = await isPortFree(targetPort);
1941
+ if (!portFree) {
1942
+ console.log(chalk8.yellow(` \u26A1 Port ${targetPort} is occupied \u2014 reclaiming...`));
1943
+ await killProcessOnPort(targetPort);
1944
+ const freed = await waitForPortFree(targetPort, 5000);
1945
+ if (!freed) {
1946
+ console.log(chalk8.red(` \u26A0 Could not free port ${targetPort} \u2014 dashboard may pick a fallback port`));
1947
+ }
1948
+ }
1949
+ }
1580
1950
  const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
1581
1951
  env: spawnEnv,
1582
1952
  cwd: bgrDir2,
@@ -1625,6 +1995,69 @@ async function run2() {
1625
1995
  announce(msg, "BGR Dashboard");
1626
1996
  return;
1627
1997
  }
1998
+ if (values.guard) {
1999
+ const guardName = "bgr-guard";
2000
+ const homePath3 = getHomeDir();
2001
+ const bgrDir2 = join3(homePath3, ".bgr");
2002
+ const existing = getProcess(guardName);
2003
+ if (existing && await isProcessRunning(existing.pid)) {
2004
+ announce(`Guard is already running (PID ${existing.pid})
2005
+
2006
+ Use ${chalk8.yellow(`bgrun --stop ${guardName}`)} to stop it
2007
+ Use ${chalk8.yellow(`bgrun --guard --force`)} to restart`, "BGR Guard");
2008
+ return;
2009
+ }
2010
+ if (existing) {
2011
+ if (await isProcessRunning(existing.pid)) {
2012
+ await terminateProcess(existing.pid);
2013
+ }
2014
+ await retryDatabaseOperation(() => removeProcessByName(guardName));
2015
+ }
2016
+ const { resolve } = __require("path");
2017
+ const scriptPath = resolve(process.argv[1]);
2018
+ const spawnCommand = `bun run ${scriptPath} --_guard-loop`;
2019
+ const command = `bgrun --_guard-loop`;
2020
+ const stdoutPath = join3(bgrDir2, `${guardName}-out.txt`);
2021
+ const stderrPath = join3(bgrDir2, `${guardName}-err.txt`);
2022
+ await Bun.write(stdoutPath, "");
2023
+ await Bun.write(stderrPath, "");
2024
+ const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
2025
+ env: { ...Bun.env },
2026
+ cwd: bgrDir2,
2027
+ stdout: Bun.file(stdoutPath),
2028
+ stderr: Bun.file(stderrPath)
2029
+ });
2030
+ newProcess.unref();
2031
+ await sleep3(1000);
2032
+ const actualPid = await findChildPid(newProcess.pid);
2033
+ await retryDatabaseOperation(() => insertProcess({
2034
+ pid: actualPid,
2035
+ workdir: bgrDir2,
2036
+ command,
2037
+ name: guardName,
2038
+ env: "BGR_KEEP_ALIVE=false",
2039
+ configPath: "",
2040
+ stdout_path: stdoutPath,
2041
+ stderr_path: stderrPath
2042
+ }));
2043
+ const msg = dedent`
2044
+ ${chalk8.bold("\uD83D\uDEE1\uFE0F BGR Standalone Guard launched")}
2045
+ ${chalk8.gray("\u2500".repeat(40))}
2046
+
2047
+ Monitors: All processes with BGR_KEEP_ALIVE=true
2048
+ Also watches: bgr-dashboard (auto-restart if it dies)
2049
+ Check interval: 30 seconds
2050
+ Backoff: Exponential after 5 rapid crashes
2051
+
2052
+ ${chalk8.gray("\u2500".repeat(40))}
2053
+ Process: ${chalk8.white(guardName)} | PID: ${chalk8.white(String(actualPid))}
2054
+
2055
+ ${chalk8.yellow(`bgrun ${guardName} --logs`)} View guard logs
2056
+ ${chalk8.yellow(`bgrun --stop ${guardName}`)} Stop the guard
2057
+ `;
2058
+ announce(msg, "BGR Guard");
2059
+ return;
2060
+ }
1628
2061
  if (values.version) {
1629
2062
  console.log(`bgrun version: ${await getVersion()}`);
1630
2063
  return;
@@ -1797,6 +2230,5 @@ async function run2() {
1797
2230
  }
1798
2231
  }
1799
2232
  run2().catch((err) => {
1800
- console.error(chalk8.red(err));
1801
- process.exit(1);
2233
+ error(err);
1802
2234
  });