bgrun 3.12.16 → 3.12.21

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/server.js CHANGED
@@ -16,49 +16,131 @@ var __export = (target, all) => {
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
  var __require = import.meta.require;
18
18
 
19
- // src/platform.ts
20
- var exports_platform = {};
21
- __export(exports_platform, {
22
- waitForPortFree: () => waitForPortFree,
23
- terminateProcess: () => terminateProcess,
24
- reconcileProcessPids: () => reconcileProcessPids,
25
- readFileTail: () => readFileTail,
26
- psExec: () => psExec,
27
- parseUnixListeningPorts: () => parseUnixListeningPorts,
28
- killProcessOnPort: () => killProcessOnPort,
29
- isWindows: () => isWindows,
30
- isProcessRunning: () => isProcessRunning,
31
- isPortFree: () => isPortFree,
32
- getShellCommand: () => getShellCommand,
33
- getProcessPorts: () => getProcessPorts,
34
- getProcessMemory: () => getProcessMemory,
35
- getProcessBatchResources: () => getProcessBatchResources,
36
- getPortInfo: () => getPortInfo,
37
- getHomeDir: () => getHomeDir,
38
- findPidByPort: () => findPidByPort,
39
- findChildPid: () => findChildPid,
40
- ensureDir: () => ensureDir,
41
- copyFile: () => copyFile
19
+ // src/log-rotation.ts
20
+ var exports_log_rotation = {};
21
+ __export(exports_log_rotation, {
22
+ startLogRotation: () => startLogRotation,
23
+ rotateLogFile: () => rotateLogFile,
24
+ rotateAllLogs: () => rotateAllLogs
42
25
  });
26
+ import { existsSync, statSync, readFileSync, writeFileSync } from "fs";
27
+ function rotateLogFile(filePath, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
28
+ try {
29
+ if (!existsSync(filePath))
30
+ return false;
31
+ const stat = statSync(filePath);
32
+ if (stat.size <= maxBytes)
33
+ return false;
34
+ const content = readFileSync(filePath, "utf-8");
35
+ const lines = content.split(`
36
+ `);
37
+ if (lines.length <= keepLines)
38
+ return false;
39
+ const truncated = lines.slice(-keepLines);
40
+ const header = `--- [bgrun] Log rotated at ${new Date().toISOString()} (was ${lines.length} lines, ${formatBytes(stat.size)}) ---
41
+ `;
42
+ writeFileSync(filePath, header + truncated.join(`
43
+ `));
44
+ return true;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+ function rotateAllLogs(getProcesses, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
50
+ const processes = getProcesses();
51
+ const rotated = [];
52
+ let checked = 0;
53
+ for (const proc of processes) {
54
+ if (proc.stdout_path) {
55
+ checked++;
56
+ if (rotateLogFile(proc.stdout_path, maxBytes, keepLines)) {
57
+ rotated.push(`${proc.name}/stdout`);
58
+ }
59
+ }
60
+ if (proc.stderr_path) {
61
+ checked++;
62
+ if (rotateLogFile(proc.stderr_path, maxBytes, keepLines)) {
63
+ rotated.push(`${proc.name}/stderr`);
64
+ }
65
+ }
66
+ }
67
+ return { rotated, checked };
68
+ }
69
+ function startLogRotation(getProcesses, intervalMs = DEFAULT_CHECK_INTERVAL_MS, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
70
+ console.log(`[logs] Log rotation active: max ${formatBytes(maxBytes)}/file, keep ${keepLines} lines, check every ${intervalMs / 1000}s`);
71
+ return setInterval(() => {
72
+ const { rotated } = rotateAllLogs(getProcesses, maxBytes, keepLines);
73
+ if (rotated.length > 0) {
74
+ console.log(`[logs] Rotated ${rotated.length} log(s): ${rotated.join(", ")}`);
75
+ }
76
+ }, intervalMs);
77
+ }
78
+ function formatBytes(bytes) {
79
+ if (bytes >= 1e6)
80
+ return `${(bytes / 1e6).toFixed(1)}MB`;
81
+ if (bytes >= 1000)
82
+ return `${(bytes / 1000).toFixed(0)}KB`;
83
+ return `${bytes}B`;
84
+ }
85
+ var DEFAULT_MAX_BYTES, DEFAULT_KEEP_LINES = 5000, DEFAULT_CHECK_INTERVAL_MS = 60000;
86
+ var init_log_rotation = __esm(() => {
87
+ DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
88
+ });
89
+
90
+ // src/platform.ts
43
91
  import * as fs from "fs";
44
92
  import * as os from "os";
45
93
  import { join } from "path";
46
94
  var {$ } = globalThis.Bun;
47
95
  import { createMeasure } from "measure-fn";
48
- function psExec(command, _timeoutMs = 3000) {
49
- const tmpFile = join(os.tmpdir(), `bgr-ps-${Date.now()}.ps1`);
96
+ async function psExec(command, timeoutMs = 3000) {
97
+ const tmpFile = join(os.tmpdir(), `bgr-ps-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.ps1`);
50
98
  try {
51
- fs.writeFileSync(tmpFile, command);
52
- const result = Bun.spawnSync(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", tmpFile]);
99
+ await Bun.write(tmpFile, command);
100
+ const proc = Bun.spawn([
101
+ "powershell",
102
+ "-NoProfile",
103
+ "-ExecutionPolicy",
104
+ "Bypass",
105
+ "-File",
106
+ tmpFile
107
+ ], {
108
+ stdout: "pipe",
109
+ stderr: "pipe"
110
+ });
111
+ const timeoutPromise = new Promise((_, reject) => {
112
+ setTimeout(() => reject(new Error("PowerShell command timed out")), timeoutMs);
113
+ });
114
+ const resultPromise = new Promise(async (resolve, reject) => {
115
+ try {
116
+ const stdoutPromise = proc.stdout ? new Response(proc.stdout).text() : Promise.resolve("");
117
+ const stderrPromise = proc.stderr ? new Response(proc.stderr).text() : Promise.resolve("");
118
+ const exitCode = await proc.exited;
119
+ const stdout = await stdoutPromise;
120
+ const stderr = await stderrPromise;
121
+ if (exitCode === 0) {
122
+ resolve(stdout);
123
+ } else {
124
+ resolve(stderr || "");
125
+ }
126
+ } catch (error) {
127
+ reject(error);
128
+ }
129
+ });
53
130
  try {
54
- fs.unlinkSync(tmpFile);
55
- } catch {}
56
- return result.stdout?.toString() || "";
57
- } catch {
131
+ const result = await Promise.race([resultPromise, timeoutPromise]);
132
+ return result.trim();
133
+ } catch (error) {
134
+ return "";
135
+ } finally {
136
+ try {
137
+ await Bun.sleep(100);
138
+ } catch {}
139
+ }
140
+ } finally {
58
141
  try {
59
- fs.unlinkSync(tmpFile);
142
+ fs.rmSync(tmpFile, { force: true });
60
143
  } catch {}
61
- return "";
62
144
  }
63
145
  }
64
146
  function isWindows() {
@@ -80,7 +162,7 @@ async function isProcessRunning(pid, command) {
80
162
  process.kill(pid, 0);
81
163
  return true;
82
164
  } catch {
83
- const output = psExec(`Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`).trim();
165
+ const output = await psExec(`Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`);
84
166
  return output === String(pid);
85
167
  }
86
168
  } else {
@@ -173,35 +255,6 @@ async function isPortFree(port) {
173
255
  return true;
174
256
  }
175
257
  }
176
- async function getPortInfo(port) {
177
- try {
178
- if (isWindows()) {
179
- const result = await $`netstat -ano | findstr :${port}`.nothrow().quiet().text();
180
- for (const line of result.split(`
181
- `)) {
182
- const match = line.match(new RegExp(`:(${port})\\s+.*LISTENING\\s+(\\d+)`));
183
- if (match) {
184
- const pid = parseInt(match[2]);
185
- if (pid > 0 && await isProcessRunning(pid)) {
186
- const nameResult = await $`powershell -NoProfile -Command "(Get-Process -Id ${pid} -ErrorAction SilentlyContinue).ProcessName"`.nothrow().quiet().text();
187
- return { inUse: true, pid, processName: nameResult.trim() || "unknown" };
188
- }
189
- }
190
- }
191
- return { inUse: false };
192
- } else {
193
- const result = await $`ss -tln sport = :${port}`.nothrow().quiet().text();
194
- const lines = result.trim().split(`
195
- `).filter((l) => l.trim());
196
- if (lines.length > 1) {
197
- return { inUse: true };
198
- }
199
- return { inUse: false };
200
- }
201
- } catch {
202
- return { inUse: false };
203
- }
204
- }
205
258
  async function waitForPortFree(port, timeoutMs = 5000) {
206
259
  const startTime = Date.now();
207
260
  const pollInterval = 300;
@@ -409,9 +462,6 @@ async function readFileTail(filePath, lines) {
409
462
  }
410
463
  }) ?? "";
411
464
  }
412
- function copyFile(src, dest) {
413
- fs.copyFileSync(src, dest);
414
- }
415
465
  async function getProcessMemory(pid) {
416
466
  const map = await getProcessBatchResources([pid]);
417
467
  return map.get(pid)?.memory || 0;
@@ -424,7 +474,7 @@ async function getProcessBatchResources(pids) {
424
474
  const pidSet = new Set(pids);
425
475
  try {
426
476
  if (isWindows()) {
427
- const output = psExec(`Get-Process -Id ${pids.join(",")} -ErrorAction SilentlyContinue | Select-Object Id, WorkingSet64 | ForEach-Object { Write-Output "$($_.Id)|$($_.WorkingSet64)" }`);
477
+ const output = await psExec(`Get-Process -Id ${pids.join(",")} -ErrorAction SilentlyContinue | Select-Object Id, WorkingSet64 | ForEach-Object { Write-Output "$($_.Id)|$($_.WorkingSet64)" }`);
428
478
  for (const line of output.split(`
429
479
  `)) {
430
480
  const sepIdx = line.indexOf("|");
@@ -504,6 +554,21 @@ async function getProcessPorts(pid) {
504
554
  return [];
505
555
  }
506
556
  }
557
+ async function resolvePidWithPorts(pid) {
558
+ const ports = await getProcessPorts(pid);
559
+ if (ports.length > 0 || !isWindows() || pid <= 0) {
560
+ return { pid, ports };
561
+ }
562
+ const childPid = await findChildPid(pid);
563
+ if (childPid === pid || childPid <= 0) {
564
+ return { pid, ports };
565
+ }
566
+ const childPorts = await getProcessPorts(childPid);
567
+ if (childPorts.length > 0) {
568
+ return { pid: childPid, ports: childPorts };
569
+ }
570
+ return { pid, ports };
571
+ }
507
572
  var plat;
508
573
  var init_platform = __esm(() => {
509
574
  plat = createMeasure("platform");
@@ -548,7 +613,7 @@ __export(exports_db, {
548
613
  import { Database, z } from "sqlite-zod-orm";
549
614
  import { join as join2 } from "path";
550
615
  var {sleep } = globalThis.Bun;
551
- import { existsSync as existsSync2, copyFileSync as copyFileSync2 } from "fs";
616
+ import { existsSync as existsSync3, copyFileSync as copyFileSync2 } from "fs";
552
617
  function shouldAutoMigrateLegacyDb() {
553
618
  const raw = (process.env.BGRUN_DISABLE_LEGACY_MIGRATION || "").trim().toLowerCase();
554
619
  return !(raw === "1" || raw === "true" || raw === "yes");
@@ -580,7 +645,7 @@ function removeProcessByName(name) {
580
645
  function updateProcessPid(name, newPid) {
581
646
  const proc = db.process.select().where({ name }).limit(1).get();
582
647
  if (proc) {
583
- db.process.update(proc.id, { pid: newPid });
648
+ proc.update({ pid: newPid });
584
649
  }
585
650
  }
586
651
  function removeAllProcesses() {
@@ -592,7 +657,7 @@ function removeAllProcesses() {
592
657
  function updateProcessEnv(name, envJson) {
593
658
  const proc = db.process.select().where({ name }).limit(1).get();
594
659
  if (proc) {
595
- db.process.update(proc.id, { env: envJson });
660
+ proc.update({ env: envJson });
596
661
  }
597
662
  }
598
663
  function getAllTemplates() {
@@ -748,7 +813,7 @@ function getDbInfo() {
748
813
  dbPath,
749
814
  bgrHome,
750
815
  dbFilename,
751
- exists: existsSync2(dbPath)
816
+ exists: existsSync3(dbPath)
752
817
  };
753
818
  }
754
819
  async function retryDatabaseOperation(operation, maxRetries = 5, delay = 100) {
@@ -807,7 +872,7 @@ var init_db = __esm(() => {
807
872
  dbPath = join2(bgrDir, dbFilename);
808
873
  bgrHome = bgrDir;
809
874
  legacyDbPath = join2(bgrDir, "bgr_v2.sqlite");
810
- if (shouldAutoMigrateLegacyDb() && !existsSync2(dbPath) && existsSync2(legacyDbPath)) {
875
+ if (shouldAutoMigrateLegacyDb() && !existsSync3(dbPath) && existsSync3(legacyDbPath)) {
811
876
  try {
812
877
  copyFileSync2(legacyDbPath, dbPath);
813
878
  console.log(`[bgrun] Migrated database: ${legacyDbPath} \u2192 ${dbPath}`);
@@ -828,486 +893,6 @@ var init_db = __esm(() => {
828
893
  });
829
894
  });
830
895
 
831
- // src/utils.ts
832
- import * as fs2 from "fs";
833
- function parseEnvString(envString) {
834
- const env = {};
835
- envString.split(",").forEach((pair) => {
836
- const [key, value] = pair.split("=");
837
- if (key && value)
838
- env[key] = value;
839
- });
840
- return env;
841
- }
842
- function calculateRuntime(startTime) {
843
- const start = new Date(startTime).getTime();
844
- const now = new Date().getTime();
845
- const diffInMinutes = Math.floor((now - start) / (1000 * 60));
846
- return `${diffInMinutes} minutes`;
847
- }
848
- async function getVersion() {
849
- try {
850
- const { join: join3 } = await import("path");
851
- const pkgPath = join3(import.meta.dir, "../package.json");
852
- const pkg = await Bun.file(pkgPath).json();
853
- return pkg.version || "0.0.0";
854
- } catch {
855
- return "0.0.0";
856
- }
857
- }
858
- function validateDirectory(directory) {
859
- if (!directory || !fs2.existsSync(directory)) {
860
- throw new Error(`Directory not found or invalid: '${directory}'`);
861
- }
862
- }
863
- function tailFile(path, prefix, colorFn, lines) {
864
- let position = 0;
865
- let lastPartial = "";
866
- if (!fs2.existsSync(path)) {
867
- return () => {};
868
- }
869
- const fd = fs2.openSync(path, "r");
870
- const printNewContent = () => {
871
- try {
872
- const stats = fs2.statSync(path);
873
- if (stats.size <= position)
874
- return;
875
- const buffer = Buffer.alloc(stats.size - position);
876
- fs2.readSync(fd, buffer, 0, buffer.length, position);
877
- let content = buffer.toString();
878
- content = lastPartial + content;
879
- lastPartial = "";
880
- const lineArray = content.split(/\r?\n/);
881
- if (!content.endsWith(`
882
- `)) {
883
- lastPartial = lineArray.pop() || "";
884
- }
885
- lineArray.forEach((line) => {
886
- if (line) {
887
- console.log(colorFn(prefix + line));
888
- }
889
- });
890
- position = stats.size;
891
- } catch (e) {}
892
- };
893
- const watcher = fs2.watch(path, { persistent: true }, (event) => {
894
- if (event === "change") {
895
- printNewContent();
896
- }
897
- });
898
- printNewContent();
899
- return () => {
900
- watcher.close();
901
- try {
902
- fs2.closeSync(fd);
903
- } catch {}
904
- };
905
- }
906
- var init_utils = __esm(() => {
907
- init_platform();
908
- });
909
-
910
- // src/deps.ts
911
- var exports_deps = {};
912
- __export(exports_deps, {
913
- getUnmetDeps: () => getUnmetDeps,
914
- getDependencies: () => getDependencies2,
915
- buildDepGraph: () => buildDepGraph
916
- });
917
- function getDependencies2(envStr) {
918
- const env = parseEnvString(envStr);
919
- const raw = env.BGR_DEPENDS_ON || "";
920
- return raw.split(",").map((s) => s.trim()).filter(Boolean);
921
- }
922
- async function buildDepGraph() {
923
- const processes = getAllProcesses();
924
- const nodeMap = new Map;
925
- for (const proc of processes) {
926
- const deps = getDependencies2(proc.env);
927
- const alive = await isProcessRunning(proc.pid, proc.command);
928
- nodeMap.set(proc.name, {
929
- name: proc.name,
930
- dependsOn: deps,
931
- dependedBy: [],
932
- running: alive,
933
- pid: proc.pid
934
- });
935
- }
936
- for (const node of nodeMap.values()) {
937
- for (const dep of node.dependsOn) {
938
- const depNode = nodeMap.get(dep);
939
- if (depNode) {
940
- depNode.dependedBy.push(node.name);
941
- }
942
- }
943
- }
944
- const inDegree = new Map;
945
- for (const node of nodeMap.values()) {
946
- inDegree.set(node.name, node.dependsOn.filter((d) => nodeMap.has(d)).length);
947
- }
948
- const queue = [];
949
- for (const [name, degree] of inDegree) {
950
- if (degree === 0)
951
- queue.push(name);
952
- }
953
- const order = [];
954
- while (queue.length > 0) {
955
- const current = queue.shift();
956
- order.push(current);
957
- const node = nodeMap.get(current);
958
- for (const dependent of node.dependedBy) {
959
- const newDegree = (inDegree.get(dependent) || 0) - 1;
960
- inDegree.set(dependent, newDegree);
961
- if (newDegree === 0)
962
- queue.push(dependent);
963
- }
964
- }
965
- const hasCycle = order.length < nodeMap.size;
966
- const cycleNodes = hasCycle ? [...nodeMap.keys()].filter((n) => !order.includes(n)) : undefined;
967
- return {
968
- nodes: [...nodeMap.values()],
969
- order,
970
- hasCycle,
971
- cycleNodes
972
- };
973
- }
974
- async function getUnmetDeps(name) {
975
- const proc = getProcess(name);
976
- if (!proc)
977
- return [];
978
- const deps = getDependencies2(proc.env);
979
- const unmet = [];
980
- for (const depName of deps) {
981
- const depProc = getProcess(depName);
982
- if (!depProc) {
983
- unmet.push(depName);
984
- continue;
985
- }
986
- const alive = await isProcessRunning(depProc.pid, depProc.command);
987
- if (!alive) {
988
- unmet.push(depName);
989
- }
990
- }
991
- return unmet;
992
- }
993
- var init_deps = __esm(() => {
994
- init_db();
995
- init_platform();
996
- init_utils();
997
- });
998
-
999
- // src/log-rotation.ts
1000
- var exports_log_rotation = {};
1001
- __export(exports_log_rotation, {
1002
- startLogRotation: () => startLogRotation,
1003
- rotateLogFile: () => rotateLogFile,
1004
- rotateAllLogs: () => rotateAllLogs
1005
- });
1006
- import { existsSync as existsSync4, statSync as statSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
1007
- function rotateLogFile(filePath, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
1008
- try {
1009
- if (!existsSync4(filePath))
1010
- return false;
1011
- const stat = statSync2(filePath);
1012
- if (stat.size <= maxBytes)
1013
- return false;
1014
- const content = readFileSync(filePath, "utf-8");
1015
- const lines = content.split(`
1016
- `);
1017
- if (lines.length <= keepLines)
1018
- return false;
1019
- const truncated = lines.slice(-keepLines);
1020
- const header = `--- [bgrun] Log rotated at ${new Date().toISOString()} (was ${lines.length} lines, ${formatBytes(stat.size)}) ---
1021
- `;
1022
- writeFileSync2(filePath, header + truncated.join(`
1023
- `));
1024
- return true;
1025
- } catch {
1026
- return false;
1027
- }
1028
- }
1029
- function rotateAllLogs(getProcesses, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
1030
- const processes = getProcesses();
1031
- const rotated = [];
1032
- let checked = 0;
1033
- for (const proc of processes) {
1034
- if (proc.stdout_path) {
1035
- checked++;
1036
- if (rotateLogFile(proc.stdout_path, maxBytes, keepLines)) {
1037
- rotated.push(`${proc.name}/stdout`);
1038
- }
1039
- }
1040
- if (proc.stderr_path) {
1041
- checked++;
1042
- if (rotateLogFile(proc.stderr_path, maxBytes, keepLines)) {
1043
- rotated.push(`${proc.name}/stderr`);
1044
- }
1045
- }
1046
- }
1047
- return { rotated, checked };
1048
- }
1049
- function startLogRotation(getProcesses, intervalMs = DEFAULT_CHECK_INTERVAL_MS, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
1050
- console.log(`[logs] Log rotation active: max ${formatBytes(maxBytes)}/file, keep ${keepLines} lines, check every ${intervalMs / 1000}s`);
1051
- return setInterval(() => {
1052
- const { rotated } = rotateAllLogs(getProcesses, maxBytes, keepLines);
1053
- if (rotated.length > 0) {
1054
- console.log(`[logs] Rotated ${rotated.length} log(s): ${rotated.join(", ")}`);
1055
- }
1056
- }, intervalMs);
1057
- }
1058
- function formatBytes(bytes) {
1059
- if (bytes >= 1e6)
1060
- return `${(bytes / 1e6).toFixed(1)}MB`;
1061
- if (bytes >= 1000)
1062
- return `${(bytes / 1000).toFixed(0)}KB`;
1063
- return `${bytes}B`;
1064
- }
1065
- var DEFAULT_MAX_BYTES, DEFAULT_KEEP_LINES = 5000, DEFAULT_CHECK_INTERVAL_MS = 60000;
1066
- var init_log_rotation = __esm(() => {
1067
- DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
1068
- });
1069
-
1070
- // src/logger.ts
1071
- import boxen from "boxen";
1072
- import chalk from "chalk";
1073
- function announce(message, title) {
1074
- console.log(boxen(message, {
1075
- padding: 1,
1076
- margin: 1,
1077
- borderColor: "green",
1078
- title: title || "bgrun",
1079
- titleAlignment: "center",
1080
- borderStyle: "round"
1081
- }));
1082
- }
1083
- function error(message) {
1084
- const text = message instanceof Error ? message.stack || message.message : String(message);
1085
- console.error(boxen(chalk.red(text), {
1086
- padding: 1,
1087
- margin: 1,
1088
- borderColor: "red",
1089
- title: "Error",
1090
- titleAlignment: "center",
1091
- borderStyle: "double"
1092
- }));
1093
- throw new BgrunError(text);
1094
- }
1095
- var BgrunError;
1096
- var init_logger = __esm(() => {
1097
- BgrunError = class BgrunError extends Error {
1098
- constructor(message) {
1099
- super(message);
1100
- this.name = "BgrunError";
1101
- }
1102
- };
1103
- });
1104
-
1105
- // src/config.ts
1106
- function formatEnvKey(key) {
1107
- return key.toUpperCase().replace(/\./g, "_");
1108
- }
1109
- function flattenConfig(obj, prefix = "") {
1110
- return Object.keys(obj).reduce((acc, key) => {
1111
- const value = obj[key];
1112
- const newPrefix = prefix ? `${prefix}.${key}` : key;
1113
- if (Array.isArray(value)) {
1114
- value.forEach((item, index) => {
1115
- const indexedPrefix = `${newPrefix}.${index}`;
1116
- if (typeof item === "object" && item !== null) {
1117
- Object.assign(acc, flattenConfig(item, indexedPrefix));
1118
- } else {
1119
- acc[formatEnvKey(indexedPrefix)] = String(item);
1120
- }
1121
- });
1122
- } else if (typeof value === "object" && value !== null) {
1123
- Object.assign(acc, flattenConfig(value, newPrefix));
1124
- } else {
1125
- acc[formatEnvKey(newPrefix)] = String(value);
1126
- }
1127
- return acc;
1128
- }, {});
1129
- }
1130
- async function parseConfigFile(configPath) {
1131
- const importPath = `${configPath}?t=${Date.now()}`;
1132
- const parsedConfig = await import(importPath).then((m) => m.default);
1133
- return flattenConfig(parsedConfig);
1134
- }
1135
-
1136
- // src/commands/run.ts
1137
- var {$: $2 } = globalThis.Bun;
1138
- var {sleep: sleep2 } = globalThis.Bun;
1139
- import { join as join3 } from "path";
1140
- import { createMeasure as createMeasure2 } from "measure-fn";
1141
- async function handleRun(options) {
1142
- const { command, directory, env, name, configPath, force, fetch, stdout, stderr } = options;
1143
- const existingProcess = name ? getProcess(name) : null;
1144
- if (name && existingProcess) {
1145
- const { getUnmetDeps: getUnmetDeps2 } = await Promise.resolve().then(() => (init_deps(), exports_deps));
1146
- const unmet = await getUnmetDeps2(name);
1147
- if (unmet.length > 0) {
1148
- await run.measure(`Start ${unmet.length} dependencies for "${name}"`, async () => {
1149
- for (const depName of unmet) {
1150
- const depProc = getProcess(depName);
1151
- if (depProc) {
1152
- announce(`\uD83D\uDCE6 Starting dependency "${depName}" for "${name}"`, "Dependency");
1153
- await handleRun({ action: "run", name: depName, force: true, remoteName: "" });
1154
- }
1155
- }
1156
- });
1157
- }
1158
- }
1159
- if (existingProcess) {
1160
- const finalDirectory2 = directory || existingProcess.workdir;
1161
- validateDirectory(finalDirectory2);
1162
- $2.cwd(finalDirectory2);
1163
- if (fetch) {
1164
- if (!__require("fs").existsSync(__require("path").join(finalDirectory2, ".git"))) {
1165
- error(`Cannot --fetch: '${finalDirectory2}' is not a Git repository.`);
1166
- }
1167
- await run.measure(`Git fetch "${name}"`, async () => {
1168
- try {
1169
- await $2`git fetch origin`;
1170
- const localHash = (await $2`git rev-parse HEAD`.text()).trim();
1171
- const remoteHash = (await $2`git rev-parse origin/$(git rev-parse --abbrev-ref HEAD)`.text()).trim();
1172
- if (localHash !== remoteHash) {
1173
- await $2`git pull origin $(git rev-parse --abbrev-ref HEAD)`;
1174
- announce("\uD83D\uDCE5 Pulled latest changes", "Git Update");
1175
- }
1176
- } catch (err) {
1177
- error(`Failed to pull latest changes: ${err}`);
1178
- }
1179
- });
1180
- }
1181
- const isRunning = await isProcessRunning(existingProcess.pid);
1182
- if (isRunning && !force) {
1183
- error(`Process '${name}' is currently running. Use --force to restart.`);
1184
- }
1185
- let detectedPorts = [];
1186
- if (isRunning) {
1187
- detectedPorts = await getProcessPorts(existingProcess.pid);
1188
- }
1189
- if (isRunning) {
1190
- await run.measure(`Terminate "${name}" (PID ${existingProcess.pid})`, async () => {
1191
- await terminateProcess(existingProcess.pid);
1192
- announce(`\uD83D\uDD25 Terminated existing process '${name}'`, "Process Terminated");
1193
- });
1194
- }
1195
- if (detectedPorts.length > 0) {
1196
- await run.measure(`Port cleanup [${detectedPorts.join(", ")}]`, async () => {
1197
- for (const port of detectedPorts) {
1198
- await killProcessOnPort(port);
1199
- }
1200
- for (const port of detectedPorts) {
1201
- const freed = await waitForPortFree(port, 5000);
1202
- if (!freed) {
1203
- await killProcessOnPort(port);
1204
- await waitForPortFree(port, 3000);
1205
- }
1206
- }
1207
- });
1208
- }
1209
- const cmdToMatch = existingProcess.command;
1210
- if (cmdToMatch) {
1211
- await run.measure("Zombie sweep", async () => {
1212
- try {
1213
- const cmdKeyword = cmdToMatch.split(" ")[1] || cmdToMatch;
1214
- const GENERIC_KEYWORDS = ["dev", "run", "start", "serve", "build", "test"];
1215
- if (GENERIC_KEYWORDS.includes(cmdKeyword.toLowerCase())) {
1216
- return;
1217
- }
1218
- const currentPid = process.pid;
1219
- const result = await psExec(`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | Where-Object { $_.CommandLine -match '${cmdKeyword.replace(/'/g, "''")}' -and $_.ProcessId -ne ${currentPid} } | Select-Object -ExpandProperty ProcessId`, 3000);
1220
- const zombiePids = result.split(`
1221
- `).map((l) => parseInt(l.trim())).filter((n) => !isNaN(n) && n > 0 && n !== currentPid);
1222
- for (const zPid of zombiePids) {
1223
- await $2`taskkill /F /T /PID ${zPid}`.nothrow().quiet();
1224
- }
1225
- if (zombiePids.length > 0) {
1226
- announce(`\uD83E\uDDF9 Swept ${zombiePids.length} zombie process(es)`, "Zombie Cleanup");
1227
- }
1228
- } catch {}
1229
- });
1230
- }
1231
- await retryDatabaseOperation(() => removeProcessByName(name));
1232
- } else {
1233
- if (!directory || !name || !command) {
1234
- error("'directory', 'name', and 'command' parameters are required for new processes.");
1235
- }
1236
- validateDirectory(directory);
1237
- $2.cwd(directory);
1238
- }
1239
- const finalCommand = command || existingProcess.command;
1240
- const finalDirectory = directory || existingProcess?.workdir;
1241
- let finalEnv = env || (existingProcess ? parseEnvString(existingProcess.env) : {});
1242
- if (!("BGR_KEEP_ALIVE" in finalEnv)) {
1243
- finalEnv.BGR_KEEP_ALIVE = "true";
1244
- }
1245
- let finalConfigPath;
1246
- if (configPath !== undefined) {
1247
- finalConfigPath = configPath;
1248
- } else if (existingProcess) {
1249
- finalConfigPath = existingProcess.configPath;
1250
- } else {
1251
- finalConfigPath = ".config.toml";
1252
- }
1253
- if (finalConfigPath) {
1254
- const fullConfigPath = join3(finalDirectory, finalConfigPath);
1255
- if (await Bun.file(fullConfigPath).exists()) {
1256
- const configEnv = await run.measure(`Parse config "${finalConfigPath}"`, async () => {
1257
- try {
1258
- return await parseConfigFile(fullConfigPath);
1259
- } catch (err) {
1260
- console.warn(`Warning: Failed to parse config file ${finalConfigPath}: ${err.message}`);
1261
- return null;
1262
- }
1263
- });
1264
- if (configEnv) {
1265
- finalEnv = { ...finalEnv, ...configEnv };
1266
- console.log(`Loaded config from ${finalConfigPath}`);
1267
- }
1268
- } else {
1269
- console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
1270
- }
1271
- }
1272
- const stdoutPath = stdout || existingProcess?.stdout_path || join3(homePath2, ".bgr", `${name}-out.txt`);
1273
- Bun.write(stdoutPath, "");
1274
- const stderrPath = stderr || existingProcess?.stderr_path || join3(homePath2, ".bgr", `${name}-err.txt`);
1275
- Bun.write(stderrPath, "");
1276
- const actualPid = await run.measure(`Spawn "${name}" \u2192 ${finalCommand}`, async () => {
1277
- const newProcess = Bun.spawn(getShellCommand(finalCommand), {
1278
- env: { ...Bun.env, ...finalEnv },
1279
- cwd: finalDirectory,
1280
- stdout: Bun.file(stdoutPath),
1281
- stderr: Bun.file(stderrPath)
1282
- });
1283
- newProcess.unref();
1284
- await sleep2(100);
1285
- const pid = await findChildPid(newProcess.pid);
1286
- await sleep2(400);
1287
- return pid;
1288
- }) ?? 0;
1289
- await retryDatabaseOperation(() => insertProcess({
1290
- pid: actualPid,
1291
- workdir: finalDirectory,
1292
- command: finalCommand,
1293
- name,
1294
- env: Object.entries(finalEnv).map(([k, v]) => `${k}=${v}`).join(","),
1295
- configPath: finalConfigPath || "",
1296
- stdout_path: stdoutPath,
1297
- stderr_path: stderrPath
1298
- }));
1299
- announce(`${existingProcess ? "\uD83D\uDD04 Restarted" : "\uD83D\uDE80 Launched"} process "${name}" with PID ${actualPid}`, "Process Started");
1300
- }
1301
- var homePath2, run;
1302
- var init_run = __esm(() => {
1303
- init_db();
1304
- init_platform();
1305
- init_logger();
1306
- init_utils();
1307
- homePath2 = getHomeDir();
1308
- run = createMeasure2("run");
1309
- });
1310
-
1311
896
  // src/server.ts
1312
897
  var exports_server = {};
1313
898
  __export(exports_server, {
@@ -1343,18 +928,20 @@ async function cleanupPort(port) {
1343
928
  async function startServer() {
1344
929
  const { start } = await import("melina");
1345
930
  const appDir = path.join(import.meta.dir, "../dashboard/app");
1346
- const requestedPort = process.env.BUN_PORT ? parseInt(process.env.BUN_PORT, 10) : 3000;
931
+ const rawRequestedPort = process.env.BUN_PORT?.trim();
932
+ const explicitPort = rawRequestedPort ? parseInt(rawRequestedPort, 10) : null;
933
+ const hasExplicitPort = explicitPort !== null && !isNaN(explicitPort) && explicitPort > 0;
934
+ const requestedPort = hasExplicitPort ? explicitPort : 3000;
1347
935
  _originalPort = requestedPort;
1348
- const resolvedPort = await cleanupPort(requestedPort);
936
+ const resolvedPort = hasExplicitPort ? await cleanupPort(requestedPort) : requestedPort;
1349
937
  _currentPort = resolvedPort;
1350
- const needsExplicitPort = process.env.BUN_PORT || resolvedPort !== requestedPort;
938
+ const needsExplicitPort = hasExplicitPort || resolvedPort !== requestedPort;
1351
939
  await start({
1352
940
  appDir,
1353
941
  defaultTitle: "bgrun Dashboard - Process Manager",
1354
942
  globalCss: path.join(appDir, "globals.css"),
1355
943
  ...needsExplicitPort && { port: resolvedPort }
1356
944
  });
1357
- startGuard();
1358
945
  const { startLogRotation: startLogRotation2 } = await Promise.resolve().then(() => (init_log_rotation(), exports_log_rotation));
1359
946
  startLogRotation2(() => getAllProcesses());
1360
947
  if (resolvedPort !== requestedPort) {
@@ -1383,92 +970,17 @@ function startStickyPortChecker() {
1383
970
  } catch {}
1384
971
  }, CHECK_INTERVAL_MS);
1385
972
  }
1386
- function startGuard() {
1387
- console.log(`[guard] \u2713 Built-in process guard started (checking every ${GUARD_INTERVAL_MS / 1000}s)`);
1388
- setInterval(async () => {
1389
- try {
1390
- const processes = getAllProcesses();
1391
- if (processes.length === 0)
1392
- return;
1393
- for (const proc of processes) {
1394
- if (GUARD_SKIP_NAMES.has(proc.name))
1395
- continue;
1396
- const env = proc.env ? parseEnvString(proc.env) : {};
1397
- if (env.BGR_KEEP_ALIVE !== "true")
1398
- continue;
1399
- const alive = await isProcessRunning(proc.pid, proc.command);
1400
- if (!alive) {
1401
- const now = Date.now();
1402
- const nextRestart = guardNextRestartTime.get(proc.name) || 0;
1403
- if (now < nextRestart)
1404
- continue;
1405
- console.log(`[guard] \u26A0 Guarded process "${proc.name}" (PID ${proc.pid}) is dead, restarting...`);
1406
- let success = false;
1407
- try {
1408
- await handleRun({
1409
- action: "run",
1410
- name: proc.name,
1411
- force: true,
1412
- remoteName: ""
1413
- });
1414
- success = true;
1415
- const prevCount = guardRestartCounts.get(proc.name) || 0;
1416
- const newCount = prevCount + 1;
1417
- guardRestartCounts.set(proc.name, newCount);
1418
- try {
1419
- addHistoryEntry(proc.name, "restart", undefined, { by: "guard", count: newCount });
1420
- } catch {}
1421
- guardEvents.unshift({ time: now, name: proc.name, action: "restart", success: true });
1422
- if (guardEvents.length > 100)
1423
- guardEvents.pop();
1424
- if (newCount > 5) {
1425
- const backoffSeconds = Math.min(30 * Math.pow(2, newCount - 6), 300);
1426
- guardNextRestartTime.set(proc.name, Date.now() + backoffSeconds * 1000);
1427
- console.log(`[guard] \u2713 Restarted "${proc.name}" (restart #${newCount}). Crash loop detected: next check delayed by ${backoffSeconds}s.`);
1428
- } else {
1429
- console.log(`[guard] \u2713 Restarted "${proc.name}" (restart #${newCount})`);
1430
- }
1431
- } catch (err) {
1432
- console.error(`[guard] \u2717 Failed to restart "${proc.name}": ${err.message}`);
1433
- guardEvents.unshift({ time: now, name: proc.name, action: "restart", success: false });
1434
- if (guardEvents.length > 100)
1435
- guardEvents.pop();
1436
- }
1437
- } else {
1438
- const prevCount = guardRestartCounts.get(proc.name) || 0;
1439
- if (prevCount > 0) {
1440
- const nextRestart = guardNextRestartTime.get(proc.name) || 0;
1441
- if (Date.now() > nextRestart + 60000) {
1442
- guardRestartCounts.delete(proc.name);
1443
- guardNextRestartTime.delete(proc.name);
1444
- }
1445
- }
1446
- }
1447
- }
1448
- } catch (err) {
1449
- console.error(`[guard] Error in guard loop: ${err.message}`);
1450
- }
1451
- }, GUARD_INTERVAL_MS);
1452
- }
1453
- var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES, _g, guardRestartCounts, guardNextRestartTime, guardEvents, _originalPort = 3000, _currentPort = 3000;
1454
- var init_server = __esm(() => {
973
+ var guardRestartCounts, guardEvents, _originalPort = 3000, _currentPort = 3000;
974
+ var init_server = __esm(async () => {
1455
975
  init_db();
1456
976
  init_platform();
1457
- init_run();
1458
- init_utils();
1459
- GUARD_SKIP_NAMES = new Set(["bgr-dashboard", "bgr-guard"]);
1460
- _g = globalThis;
1461
- if (!_g.__bgrGuardRestartCounts)
1462
- _g.__bgrGuardRestartCounts = new Map;
1463
- if (!_g.__bgrGuardNextRestartTime)
1464
- _g.__bgrGuardNextRestartTime = new Map;
1465
- if (!_g.__bgrGuardEvents)
1466
- _g.__bgrGuardEvents = [];
1467
- guardRestartCounts = _g.__bgrGuardRestartCounts;
1468
- guardNextRestartTime = _g.__bgrGuardNextRestartTime;
1469
- guardEvents = _g.__bgrGuardEvents;
977
+ guardRestartCounts = new Map;
978
+ guardEvents = [];
979
+ if (import.meta.main) {
980
+ await startServer();
981
+ }
1470
982
  });
1471
- init_server();
983
+ await init_server();
1472
984
 
1473
985
  export {
1474
986
  startServer,