bgrun 3.10.2 → 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) {
@@ -528,7 +538,7 @@ var init_db = __esm(() => {
528
538
  import boxen from "boxen";
529
539
  import chalk2 from "chalk";
530
540
  function announce(message, title) {
531
- console.log(boxen(chalk2.white(message), {
541
+ console.log(boxen(message, {
532
542
  padding: 1,
533
543
  margin: 1,
534
544
  borderColor: "green",
@@ -538,7 +548,8 @@ function announce(message, title) {
538
548
  }));
539
549
  }
540
550
  function error(message) {
541
- 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), {
542
553
  padding: 1,
543
554
  margin: 1,
544
555
  borderColor: "red",
@@ -581,6 +592,95 @@ async function parseConfigFile(configPath) {
581
592
  return flattenConfig(parsedConfig);
582
593
  }
583
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
+
584
684
  // src/commands/run.ts
585
685
  var {$: $2 } = globalThis.Bun;
586
686
  var {sleep: sleep2 } = globalThis.Bun;
@@ -589,6 +689,21 @@ import { createMeasure as createMeasure2 } from "measure-fn";
589
689
  async function handleRun(options) {
590
690
  const { command, directory, env, name, configPath, force, fetch, stdout, stderr } = options;
591
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
+ }
592
707
  if (existingProcess) {
593
708
  const finalDirectory2 = directory || existingProcess.workdir;
594
709
  validateDirectory(finalDirectory2);
@@ -732,10 +847,82 @@ var init_run = __esm(() => {
732
847
  run = createMeasure2("run");
733
848
  });
734
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
+
735
921
  // src/server.ts
736
922
  var exports_server = {};
737
923
  __export(exports_server, {
738
- startServer: () => startServer
924
+ startServer: () => startServer,
925
+ guardRestartCounts: () => guardRestartCounts
739
926
  });
740
927
  import path2 from "path";
741
928
  async function startServer() {
@@ -749,6 +936,8 @@ async function startServer() {
749
936
  ...explicitPort !== undefined && { port: explicitPort }
750
937
  });
751
938
  startGuard();
939
+ const { startLogRotation: startLogRotation2 } = await Promise.resolve().then(() => (init_log_rotation(), exports_log_rotation));
940
+ startLogRotation2(() => getAllProcesses());
752
941
  }
753
942
  function startGuard() {
754
943
  console.log(`[guard] \u2713 Built-in process guard started (checking every ${GUARD_INTERVAL_MS / 1000}s)`);
@@ -760,17 +949,15 @@ function startGuard() {
760
949
  for (const proc of processes) {
761
950
  if (GUARD_SKIP_NAMES.has(proc.name))
762
951
  continue;
763
- const env = proc.env ? typeof proc.env === "string" ? (() => {
764
- try {
765
- return JSON.parse(proc.env);
766
- } catch {
767
- return {};
768
- }
769
- })() : proc.env : {};
952
+ const env = proc.env ? parseEnvString(proc.env) : {};
770
953
  if (env.BGR_KEEP_ALIVE !== "true")
771
954
  continue;
772
955
  const alive = await isProcessRunning(proc.pid, proc.command);
773
956
  if (!alive) {
957
+ const now = Date.now();
958
+ const nextRestart = guardNextRestartTime.get(proc.name) || 0;
959
+ if (now < nextRestart)
960
+ continue;
774
961
  console.log(`[guard] \u26A0 Guarded process "${proc.name}" (PID ${proc.pid}) is dead, restarting...`);
775
962
  try {
776
963
  await handleRun({
@@ -779,10 +966,28 @@ function startGuard() {
779
966
  force: true,
780
967
  remoteName: ""
781
968
  });
782
- 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
+ }
783
979
  } catch (err) {
784
980
  console.error(`[guard] \u2717 Failed to restart "${proc.name}": ${err.message}`);
785
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
+ }
786
991
  }
787
992
  }
788
993
  } catch (err) {
@@ -790,12 +995,139 @@ function startGuard() {
790
995
  }
791
996
  }, GUARD_INTERVAL_MS);
792
997
  }
793
- var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES;
998
+ var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES, _g, guardRestartCounts, guardNextRestartTime;
794
999
  var init_server = __esm(() => {
795
1000
  init_db();
796
1001
  init_platform();
797
1002
  init_run();
798
- 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
+ };
799
1131
  });
800
1132
 
801
1133
  // src/index.ts
@@ -1009,11 +1341,11 @@ async function showAll(opts) {
1009
1341
  }
1010
1342
  const tableData = [];
1011
1343
  const allPids = filtered.map((p) => p.pid);
1012
- const memoryMap = await getProcessBatchMemory(allPids);
1344
+ const resourceMap = await getProcessBatchResources(allPids);
1013
1345
  for (const proc of filtered) {
1014
1346
  const isRunning = await isProcessRunning(proc.pid, proc.command);
1015
1347
  const runtime = calculateRuntime(proc.timestamp);
1016
- const mem = isRunning ? memoryMap.get(proc.pid) || 0 : 0;
1348
+ const mem = isRunning ? resourceMap.get(proc.pid)?.memory || 0 : 0;
1017
1349
  const ports = isRunning ? await getProcessPorts(proc.pid) : [];
1018
1350
  tableData.push({
1019
1351
  id: proc.id,
@@ -1470,6 +1802,7 @@ async function showHelp() {
1470
1802
  bgrun List all processes
1471
1803
  bgrun [name] Show details for a process
1472
1804
  bgrun --dashboard Launch web dashboard (managed by bgrun)
1805
+ bgrun --guard Launch standalone process guard
1473
1806
  bgrun --restart [name] Restart a process
1474
1807
  bgrun --restart-all Restart ALL registered processes
1475
1808
  bgrun --stop [name] Stop a process (keep in registry)
@@ -1535,8 +1868,10 @@ async function run2() {
1535
1868
  stdout: { type: "string" },
1536
1869
  stderr: { type: "string" },
1537
1870
  dashboard: { type: "boolean" },
1871
+ guard: { type: "boolean" },
1538
1872
  debug: { type: "boolean" },
1539
1873
  _serve: { type: "boolean" },
1874
+ "_guard-loop": { type: "boolean" },
1540
1875
  port: { type: "string" }
1541
1876
  },
1542
1877
  strict: false,
@@ -1547,6 +1882,13 @@ async function run2() {
1547
1882
  await startServer2();
1548
1883
  return;
1549
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
+ }
1550
1892
  if (values.dashboard) {
1551
1893
  const dashboardName = "bgr-dashboard";
1552
1894
  const homePath3 = getHomeDir();
@@ -1593,6 +1935,18 @@ async function run2() {
1593
1935
  if (requestedPort) {
1594
1936
  spawnEnv.BUN_PORT = requestedPort;
1595
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
+ }
1596
1950
  const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
1597
1951
  env: spawnEnv,
1598
1952
  cwd: bgrDir2,
@@ -1641,6 +1995,69 @@ async function run2() {
1641
1995
  announce(msg, "BGR Dashboard");
1642
1996
  return;
1643
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
+ }
1644
2061
  if (values.version) {
1645
2062
  console.log(`bgrun version: ${await getVersion()}`);
1646
2063
  return;
@@ -1813,6 +2230,5 @@ async function run2() {
1813
2230
  }
1814
2231
  }
1815
2232
  run2().catch((err) => {
1816
- console.error(chalk8.red(err));
1817
- process.exit(1);
2233
+ error(err);
1818
2234
  });