bgrun 3.12.15 → 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
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;
42
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, {
@@ -1320,33 +905,22 @@ async function cleanupPort(port) {
1320
905
  if (process.platform !== "win32")
1321
906
  return port;
1322
907
  try {
1323
- const proc = Bun.spawn([
1324
- "powershell",
1325
- "-NoProfile",
1326
- "-Command",
1327
- `Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
1328
- ], { stdout: "pipe", stderr: "pipe" });
1329
- const text = await new Response(proc.stdout).text();
1330
- const pid = parseInt(text.trim(), 10);
1331
- if (!pid || pid === process.pid)
908
+ const occupiedBefore = !await isPortFree(port);
909
+ if (!occupiedBefore)
1332
910
  return port;
1333
- const checkProc = Bun.spawn([
1334
- "powershell",
1335
- "-NoProfile",
1336
- "-Command",
1337
- `Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`
1338
- ], { stdout: "pipe", stderr: "pipe" });
1339
- const checkText = await new Response(checkProc.stdout).text();
1340
- if (checkText.trim()) {
1341
- console.log(`[server] Killing PID ${pid} holding port ${port}`);
1342
- Bun.spawn(["taskkill", "/F", "/PID", String(pid)], { stdout: "pipe", stderr: "pipe" });
1343
- await Bun.sleep(1000);
911
+ console.log(`[server] Reclaiming port ${port} before dashboard start`);
912
+ await killProcessOnPort(port);
913
+ const freed = await waitForPortFree(port, 8000);
914
+ if (freed)
1344
915
  return port;
1345
- } else {
1346
- const fallback = port + 1;
1347
- console.log(`[server] \u26A0 Port ${port} held by zombie PID ${pid} \u2014 falling back to port ${fallback}`);
1348
- return fallback;
1349
- }
916
+ console.warn(`[server] Port ${port} still busy after first cleanup; retrying`);
917
+ await killProcessOnPort(port);
918
+ const freedAfterRetry = await waitForPortFree(port, 5000);
919
+ if (freedAfterRetry)
920
+ return port;
921
+ const fallback = port + 1;
922
+ console.warn(`[server] \u26A0 Could not reclaim port ${port}; falling back to port ${fallback}`);
923
+ return fallback;
1350
924
  } catch {
1351
925
  return port;
1352
926
  }
@@ -1354,18 +928,20 @@ async function cleanupPort(port) {
1354
928
  async function startServer() {
1355
929
  const { start } = await import("melina");
1356
930
  const appDir = path.join(import.meta.dir, "../dashboard/app");
1357
- 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;
1358
935
  _originalPort = requestedPort;
1359
- const resolvedPort = await cleanupPort(requestedPort);
936
+ const resolvedPort = hasExplicitPort ? await cleanupPort(requestedPort) : requestedPort;
1360
937
  _currentPort = resolvedPort;
1361
- const needsExplicitPort = process.env.BUN_PORT || resolvedPort !== requestedPort;
938
+ const needsExplicitPort = hasExplicitPort || resolvedPort !== requestedPort;
1362
939
  await start({
1363
940
  appDir,
1364
941
  defaultTitle: "bgrun Dashboard - Process Manager",
1365
942
  globalCss: path.join(appDir, "globals.css"),
1366
943
  ...needsExplicitPort && { port: resolvedPort }
1367
944
  });
1368
- startGuard();
1369
945
  const { startLogRotation: startLogRotation2 } = await Promise.resolve().then(() => (init_log_rotation(), exports_log_rotation));
1370
946
  startLogRotation2(() => getAllProcesses());
1371
947
  if (resolvedPort !== requestedPort) {
@@ -1394,92 +970,17 @@ function startStickyPortChecker() {
1394
970
  } catch {}
1395
971
  }, CHECK_INTERVAL_MS);
1396
972
  }
1397
- function startGuard() {
1398
- console.log(`[guard] \u2713 Built-in process guard started (checking every ${GUARD_INTERVAL_MS / 1000}s)`);
1399
- setInterval(async () => {
1400
- try {
1401
- const processes = getAllProcesses();
1402
- if (processes.length === 0)
1403
- return;
1404
- for (const proc of processes) {
1405
- if (GUARD_SKIP_NAMES.has(proc.name))
1406
- continue;
1407
- const env = proc.env ? parseEnvString(proc.env) : {};
1408
- if (env.BGR_KEEP_ALIVE !== "true")
1409
- continue;
1410
- const alive = await isProcessRunning(proc.pid, proc.command);
1411
- if (!alive) {
1412
- const now = Date.now();
1413
- const nextRestart = guardNextRestartTime.get(proc.name) || 0;
1414
- if (now < nextRestart)
1415
- continue;
1416
- console.log(`[guard] \u26A0 Guarded process "${proc.name}" (PID ${proc.pid}) is dead, restarting...`);
1417
- let success = false;
1418
- try {
1419
- await handleRun({
1420
- action: "run",
1421
- name: proc.name,
1422
- force: true,
1423
- remoteName: ""
1424
- });
1425
- success = true;
1426
- const prevCount = guardRestartCounts.get(proc.name) || 0;
1427
- const newCount = prevCount + 1;
1428
- guardRestartCounts.set(proc.name, newCount);
1429
- try {
1430
- addHistoryEntry(proc.name, "restart", undefined, { by: "guard", count: newCount });
1431
- } catch {}
1432
- guardEvents.unshift({ time: now, name: proc.name, action: "restart", success: true });
1433
- if (guardEvents.length > 100)
1434
- guardEvents.pop();
1435
- if (newCount > 5) {
1436
- const backoffSeconds = Math.min(30 * Math.pow(2, newCount - 6), 300);
1437
- guardNextRestartTime.set(proc.name, Date.now() + backoffSeconds * 1000);
1438
- console.log(`[guard] \u2713 Restarted "${proc.name}" (restart #${newCount}). Crash loop detected: next check delayed by ${backoffSeconds}s.`);
1439
- } else {
1440
- console.log(`[guard] \u2713 Restarted "${proc.name}" (restart #${newCount})`);
1441
- }
1442
- } catch (err) {
1443
- console.error(`[guard] \u2717 Failed to restart "${proc.name}": ${err.message}`);
1444
- guardEvents.unshift({ time: now, name: proc.name, action: "restart", success: false });
1445
- if (guardEvents.length > 100)
1446
- guardEvents.pop();
1447
- }
1448
- } else {
1449
- const prevCount = guardRestartCounts.get(proc.name) || 0;
1450
- if (prevCount > 0) {
1451
- const nextRestart = guardNextRestartTime.get(proc.name) || 0;
1452
- if (Date.now() > nextRestart + 60000) {
1453
- guardRestartCounts.delete(proc.name);
1454
- guardNextRestartTime.delete(proc.name);
1455
- }
1456
- }
1457
- }
1458
- }
1459
- } catch (err) {
1460
- console.error(`[guard] Error in guard loop: ${err.message}`);
1461
- }
1462
- }, GUARD_INTERVAL_MS);
1463
- }
1464
- var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES, _g, guardRestartCounts, guardNextRestartTime, guardEvents, _originalPort = 3000, _currentPort = 3000;
1465
- var init_server = __esm(() => {
973
+ var guardRestartCounts, guardEvents, _originalPort = 3000, _currentPort = 3000;
974
+ var init_server = __esm(async () => {
1466
975
  init_db();
1467
976
  init_platform();
1468
- init_run();
1469
- init_utils();
1470
- GUARD_SKIP_NAMES = new Set(["bgr-dashboard", "bgr-guard"]);
1471
- _g = globalThis;
1472
- if (!_g.__bgrGuardRestartCounts)
1473
- _g.__bgrGuardRestartCounts = new Map;
1474
- if (!_g.__bgrGuardNextRestartTime)
1475
- _g.__bgrGuardNextRestartTime = new Map;
1476
- if (!_g.__bgrGuardEvents)
1477
- _g.__bgrGuardEvents = [];
1478
- guardRestartCounts = _g.__bgrGuardRestartCounts;
1479
- guardNextRestartTime = _g.__bgrGuardNextRestartTime;
1480
- guardEvents = _g.__bgrGuardEvents;
977
+ guardRestartCounts = new Map;
978
+ guardEvents = [];
979
+ if (import.meta.main) {
980
+ await startServer();
981
+ }
1481
982
  });
1482
- init_server();
983
+ await init_server();
1483
984
 
1484
985
  export {
1485
986
  startServer,