bgrun 3.12.16 → 3.12.22
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/README.md +54 -15
- package/dashboard/app/api/guard/route.ts +5 -13
- package/dashboard/app/api/guard-all/route.ts +6 -14
- package/dashboard/app/api/guard-events/route.ts +3 -3
- package/dashboard/app/api/next-port/route.ts +27 -7
- package/dashboard/app/api/processes/route.ts +31 -5
- package/dashboard/app/page.client.tsx +2 -19
- package/dashboard/app/page.tsx +1 -5
- package/dashboard/lib/runtime.ts +58 -49
- package/dist/api.js +912 -234
- package/dist/deploy.js +691 -231
- package/dist/deps.js +151 -70
- package/dist/index.js +1276 -717
- package/dist/server.js +151 -639
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -18,48 +18,59 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
|
18
18
|
var __require = import.meta.require;
|
|
19
19
|
|
|
20
20
|
// src/platform.ts
|
|
21
|
-
var exports_platform = {};
|
|
22
|
-
__export(exports_platform, {
|
|
23
|
-
waitForPortFree: () => waitForPortFree,
|
|
24
|
-
terminateProcess: () => terminateProcess,
|
|
25
|
-
reconcileProcessPids: () => reconcileProcessPids,
|
|
26
|
-
readFileTail: () => readFileTail,
|
|
27
|
-
psExec: () => psExec,
|
|
28
|
-
parseUnixListeningPorts: () => parseUnixListeningPorts,
|
|
29
|
-
killProcessOnPort: () => killProcessOnPort,
|
|
30
|
-
isWindows: () => isWindows,
|
|
31
|
-
isProcessRunning: () => isProcessRunning,
|
|
32
|
-
isPortFree: () => isPortFree,
|
|
33
|
-
getShellCommand: () => getShellCommand,
|
|
34
|
-
getProcessPorts: () => getProcessPorts,
|
|
35
|
-
getProcessMemory: () => getProcessMemory,
|
|
36
|
-
getProcessBatchResources: () => getProcessBatchResources,
|
|
37
|
-
getPortInfo: () => getPortInfo,
|
|
38
|
-
getHomeDir: () => getHomeDir,
|
|
39
|
-
findPidByPort: () => findPidByPort,
|
|
40
|
-
findChildPid: () => findChildPid,
|
|
41
|
-
ensureDir: () => ensureDir,
|
|
42
|
-
copyFile: () => copyFile
|
|
43
|
-
});
|
|
44
21
|
import * as fs from "fs";
|
|
45
22
|
import * as os from "os";
|
|
46
23
|
import { join } from "path";
|
|
47
24
|
var {$ } = globalThis.Bun;
|
|
48
25
|
import { createMeasure } from "measure-fn";
|
|
49
|
-
function psExec(command,
|
|
50
|
-
const tmpFile = join(os.tmpdir(), `bgr-ps-${Date.now()}.ps1`);
|
|
26
|
+
async function psExec(command, timeoutMs = 3000) {
|
|
27
|
+
const tmpFile = join(os.tmpdir(), `bgr-ps-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.ps1`);
|
|
51
28
|
try {
|
|
52
|
-
|
|
53
|
-
const
|
|
29
|
+
await Bun.write(tmpFile, command);
|
|
30
|
+
const proc = Bun.spawn([
|
|
31
|
+
"powershell",
|
|
32
|
+
"-NoProfile",
|
|
33
|
+
"-ExecutionPolicy",
|
|
34
|
+
"Bypass",
|
|
35
|
+
"-File",
|
|
36
|
+
tmpFile
|
|
37
|
+
], {
|
|
38
|
+
stdout: "pipe",
|
|
39
|
+
stderr: "pipe"
|
|
40
|
+
});
|
|
41
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
42
|
+
setTimeout(() => reject(new Error("PowerShell command timed out")), timeoutMs);
|
|
43
|
+
});
|
|
44
|
+
const resultPromise = new Promise(async (resolve, reject) => {
|
|
45
|
+
try {
|
|
46
|
+
const stdoutPromise = proc.stdout ? new Response(proc.stdout).text() : Promise.resolve("");
|
|
47
|
+
const stderrPromise = proc.stderr ? new Response(proc.stderr).text() : Promise.resolve("");
|
|
48
|
+
const exitCode = await proc.exited;
|
|
49
|
+
const stdout = await stdoutPromise;
|
|
50
|
+
const stderr = await stderrPromise;
|
|
51
|
+
if (exitCode === 0) {
|
|
52
|
+
resolve(stdout);
|
|
53
|
+
} else {
|
|
54
|
+
resolve(stderr || "");
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
reject(error);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
54
60
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
const result = await Promise.race([resultPromise, timeoutPromise]);
|
|
62
|
+
return result.trim();
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return "";
|
|
65
|
+
} finally {
|
|
66
|
+
try {
|
|
67
|
+
await Bun.sleep(100);
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
70
|
+
} finally {
|
|
59
71
|
try {
|
|
60
|
-
fs.
|
|
72
|
+
fs.rmSync(tmpFile, { force: true });
|
|
61
73
|
} catch {}
|
|
62
|
-
return "";
|
|
63
74
|
}
|
|
64
75
|
}
|
|
65
76
|
function isWindows() {
|
|
@@ -81,7 +92,7 @@ async function isProcessRunning(pid, command) {
|
|
|
81
92
|
process.kill(pid, 0);
|
|
82
93
|
return true;
|
|
83
94
|
} catch {
|
|
84
|
-
const output = psExec(`Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`)
|
|
95
|
+
const output = await psExec(`Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`);
|
|
85
96
|
return output === String(pid);
|
|
86
97
|
}
|
|
87
98
|
} else {
|
|
@@ -174,35 +185,6 @@ async function isPortFree(port) {
|
|
|
174
185
|
return true;
|
|
175
186
|
}
|
|
176
187
|
}
|
|
177
|
-
async function getPortInfo(port) {
|
|
178
|
-
try {
|
|
179
|
-
if (isWindows()) {
|
|
180
|
-
const result = await $`netstat -ano | findstr :${port}`.nothrow().quiet().text();
|
|
181
|
-
for (const line of result.split(`
|
|
182
|
-
`)) {
|
|
183
|
-
const match = line.match(new RegExp(`:(${port})\\s+.*LISTENING\\s+(\\d+)`));
|
|
184
|
-
if (match) {
|
|
185
|
-
const pid = parseInt(match[2]);
|
|
186
|
-
if (pid > 0 && await isProcessRunning(pid)) {
|
|
187
|
-
const nameResult = await $`powershell -NoProfile -Command "(Get-Process -Id ${pid} -ErrorAction SilentlyContinue).ProcessName"`.nothrow().quiet().text();
|
|
188
|
-
return { inUse: true, pid, processName: nameResult.trim() || "unknown" };
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return { inUse: false };
|
|
193
|
-
} else {
|
|
194
|
-
const result = await $`ss -tln sport = :${port}`.nothrow().quiet().text();
|
|
195
|
-
const lines = result.trim().split(`
|
|
196
|
-
`).filter((l) => l.trim());
|
|
197
|
-
if (lines.length > 1) {
|
|
198
|
-
return { inUse: true };
|
|
199
|
-
}
|
|
200
|
-
return { inUse: false };
|
|
201
|
-
}
|
|
202
|
-
} catch {
|
|
203
|
-
return { inUse: false };
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
188
|
async function waitForPortFree(port, timeoutMs = 5000) {
|
|
207
189
|
const startTime = Date.now();
|
|
208
190
|
const pollInterval = 300;
|
|
@@ -410,9 +392,6 @@ async function readFileTail(filePath, lines) {
|
|
|
410
392
|
}
|
|
411
393
|
}) ?? "";
|
|
412
394
|
}
|
|
413
|
-
function copyFile(src, dest) {
|
|
414
|
-
fs.copyFileSync(src, dest);
|
|
415
|
-
}
|
|
416
395
|
async function getProcessMemory(pid) {
|
|
417
396
|
const map = await getProcessBatchResources([pid]);
|
|
418
397
|
return map.get(pid)?.memory || 0;
|
|
@@ -425,7 +404,7 @@ async function getProcessBatchResources(pids) {
|
|
|
425
404
|
const pidSet = new Set(pids);
|
|
426
405
|
try {
|
|
427
406
|
if (isWindows()) {
|
|
428
|
-
const output = psExec(`Get-Process -Id ${pids.join(",")} -ErrorAction SilentlyContinue | Select-Object Id, WorkingSet64 | ForEach-Object { Write-Output "$($_.Id)|$($_.WorkingSet64)" }`);
|
|
407
|
+
const output = await psExec(`Get-Process -Id ${pids.join(",")} -ErrorAction SilentlyContinue | Select-Object Id, WorkingSet64 | ForEach-Object { Write-Output "$($_.Id)|$($_.WorkingSet64)" }`);
|
|
429
408
|
for (const line of output.split(`
|
|
430
409
|
`)) {
|
|
431
410
|
const sepIdx = line.indexOf("|");
|
|
@@ -505,6 +484,21 @@ async function getProcessPorts(pid) {
|
|
|
505
484
|
return [];
|
|
506
485
|
}
|
|
507
486
|
}
|
|
487
|
+
async function resolvePidWithPorts(pid) {
|
|
488
|
+
const ports = await getProcessPorts(pid);
|
|
489
|
+
if (ports.length > 0 || !isWindows() || pid <= 0) {
|
|
490
|
+
return { pid, ports };
|
|
491
|
+
}
|
|
492
|
+
const childPid = await findChildPid(pid);
|
|
493
|
+
if (childPid === pid || childPid <= 0) {
|
|
494
|
+
return { pid, ports };
|
|
495
|
+
}
|
|
496
|
+
const childPorts = await getProcessPorts(childPid);
|
|
497
|
+
if (childPorts.length > 0) {
|
|
498
|
+
return { pid: childPid, ports: childPorts };
|
|
499
|
+
}
|
|
500
|
+
return { pid, ports };
|
|
501
|
+
}
|
|
508
502
|
var plat;
|
|
509
503
|
var init_platform = __esm(() => {
|
|
510
504
|
plat = createMeasure("platform");
|
|
@@ -581,7 +575,7 @@ function removeProcessByName(name) {
|
|
|
581
575
|
function updateProcessPid(name, newPid) {
|
|
582
576
|
const proc = db.process.select().where({ name }).limit(1).get();
|
|
583
577
|
if (proc) {
|
|
584
|
-
|
|
578
|
+
proc.update({ pid: newPid });
|
|
585
579
|
}
|
|
586
580
|
}
|
|
587
581
|
function removeAllProcesses() {
|
|
@@ -593,7 +587,7 @@ function removeAllProcesses() {
|
|
|
593
587
|
function updateProcessEnv(name, envJson) {
|
|
594
588
|
const proc = db.process.select().where({ name }).limit(1).get();
|
|
595
589
|
if (proc) {
|
|
596
|
-
|
|
590
|
+
proc.update({ env: envJson });
|
|
597
591
|
}
|
|
598
592
|
}
|
|
599
593
|
function getAllTemplates() {
|
|
@@ -831,6 +825,8 @@ var init_db = __esm(() => {
|
|
|
831
825
|
|
|
832
826
|
// src/utils.ts
|
|
833
827
|
import * as fs2 from "fs";
|
|
828
|
+
import * as os2 from "os";
|
|
829
|
+
import { join as join3 } from "path";
|
|
834
830
|
function parseEnvString(envString) {
|
|
835
831
|
const env = {};
|
|
836
832
|
envString.split(",").forEach((pair) => {
|
|
@@ -846,9 +842,92 @@ function calculateRuntime(startTime) {
|
|
|
846
842
|
const diffInMinutes = Math.floor((now - start) / (1000 * 60));
|
|
847
843
|
return `${diffInMinutes} minutes`;
|
|
848
844
|
}
|
|
845
|
+
function parseCommandEnv(command) {
|
|
846
|
+
const env = {};
|
|
847
|
+
const trimmed = command.trim();
|
|
848
|
+
const windowsSegments = trimmed.split(/&&/).map((segment) => segment.trim());
|
|
849
|
+
for (const segment of windowsSegments) {
|
|
850
|
+
const match = segment.match(/^set\s+([A-Za-z_][A-Za-z0-9_]*)=(.*)$/i);
|
|
851
|
+
if (!match)
|
|
852
|
+
break;
|
|
853
|
+
env[match[1]] = match[2].trim();
|
|
854
|
+
}
|
|
855
|
+
const unixPrefixRegex = /^(?:([A-Za-z_][A-Za-z0-9_]*)=([^\s]+)\s+)+/;
|
|
856
|
+
const unixPrefix = trimmed.match(unixPrefixRegex);
|
|
857
|
+
if (unixPrefix) {
|
|
858
|
+
const pairs = unixPrefix[0].trim().split(/\s+/);
|
|
859
|
+
for (const pair of pairs) {
|
|
860
|
+
const eqIdx = pair.indexOf("=");
|
|
861
|
+
if (eqIdx <= 0)
|
|
862
|
+
continue;
|
|
863
|
+
const key = pair.slice(0, eqIdx);
|
|
864
|
+
const value = pair.slice(eqIdx + 1);
|
|
865
|
+
if (key)
|
|
866
|
+
env[key] = value;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return env;
|
|
870
|
+
}
|
|
871
|
+
function getDeclaredPort(processEnv, command) {
|
|
872
|
+
const mergedEnv = { ...command ? parseCommandEnv(command) : {}, ...processEnv };
|
|
873
|
+
const raw = mergedEnv.PORT || mergedEnv.BUN_PORT || "";
|
|
874
|
+
const parsed = parseInt(raw, 10);
|
|
875
|
+
return !isNaN(parsed) && parsed > 0 ? parsed : null;
|
|
876
|
+
}
|
|
877
|
+
function buildManagedProcessEnv(parentEnv, processEnv = {}) {
|
|
878
|
+
const sanitizedParentEnv = {};
|
|
879
|
+
for (const [key, value] of Object.entries(parentEnv)) {
|
|
880
|
+
if (value === undefined)
|
|
881
|
+
continue;
|
|
882
|
+
if (INTERNAL_MANAGED_ENV_KEYS.includes(key))
|
|
883
|
+
continue;
|
|
884
|
+
sanitizedParentEnv[key] = value;
|
|
885
|
+
}
|
|
886
|
+
return { ...sanitizedParentEnv, ...processEnv };
|
|
887
|
+
}
|
|
888
|
+
function stringifyEnvString(env) {
|
|
889
|
+
return Object.entries(env).map(([key, value]) => `${key}=${value}`).join(",");
|
|
890
|
+
}
|
|
891
|
+
function getWatcherProcessName(targetName) {
|
|
892
|
+
return `${WATCHER_PREFIX}${encodeURIComponent(targetName)}`;
|
|
893
|
+
}
|
|
894
|
+
function getWatchedProcessName(watcherName) {
|
|
895
|
+
if (!watcherName.startsWith(WATCHER_PREFIX))
|
|
896
|
+
return null;
|
|
897
|
+
try {
|
|
898
|
+
return decodeURIComponent(watcherName.slice(WATCHER_PREFIX.length));
|
|
899
|
+
} catch {
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
function isWatcherProcessName(name) {
|
|
904
|
+
return getWatchedProcessName(name) !== null;
|
|
905
|
+
}
|
|
906
|
+
function isInternalProcessName(name) {
|
|
907
|
+
return name === "bgr-dashboard" || name === "bgr-guard" || isWatcherProcessName(name);
|
|
908
|
+
}
|
|
909
|
+
function getOperationLockPath(name) {
|
|
910
|
+
return join3(os2.homedir(), ".bgr", `${name}.operation.lock`);
|
|
911
|
+
}
|
|
912
|
+
function acquireProcessOperationLock(name) {
|
|
913
|
+
const lockPath = getOperationLockPath(name);
|
|
914
|
+
fs2.mkdirSync(join3(os2.homedir(), ".bgr"), { recursive: true });
|
|
915
|
+
fs2.writeFileSync(lockPath, JSON.stringify({ pid: process.pid, time: Date.now() }));
|
|
916
|
+
let released = false;
|
|
917
|
+
return () => {
|
|
918
|
+
if (released)
|
|
919
|
+
return;
|
|
920
|
+
released = true;
|
|
921
|
+
try {
|
|
922
|
+
fs2.unlinkSync(lockPath);
|
|
923
|
+
} catch {}
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
function isProcessOperationLocked(name) {
|
|
927
|
+
return fs2.existsSync(getOperationLockPath(name));
|
|
928
|
+
}
|
|
849
929
|
async function getVersion() {
|
|
850
930
|
try {
|
|
851
|
-
const { join: join3 } = await import("path");
|
|
852
931
|
const pkgPath = join3(import.meta.dir, "../package.json");
|
|
853
932
|
const pkg = await Bun.file(pkgPath).json();
|
|
854
933
|
return pkg.version || "0.0.0";
|
|
@@ -904,8 +983,10 @@ function tailFile(path, prefix, colorFn, lines) {
|
|
|
904
983
|
} catch {}
|
|
905
984
|
};
|
|
906
985
|
}
|
|
986
|
+
var INTERNAL_MANAGED_ENV_KEYS, WATCHER_PREFIX = "bgr-watch-";
|
|
907
987
|
var init_utils = __esm(() => {
|
|
908
988
|
init_platform();
|
|
989
|
+
INTERNAL_MANAGED_ENV_KEYS = ["BUN_PORT", "BGR_STDOUT", "BGR_STDERR"];
|
|
909
990
|
});
|
|
910
991
|
|
|
911
992
|
// src/deps.ts
|
|
@@ -1068,247 +1149,6 @@ var init_log_rotation = __esm(() => {
|
|
|
1068
1149
|
DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
|
1069
1150
|
});
|
|
1070
1151
|
|
|
1071
|
-
// src/logger.ts
|
|
1072
|
-
import boxen from "boxen";
|
|
1073
|
-
import chalk from "chalk";
|
|
1074
|
-
function announce(message, title) {
|
|
1075
|
-
console.log(boxen(message, {
|
|
1076
|
-
padding: 1,
|
|
1077
|
-
margin: 1,
|
|
1078
|
-
borderColor: "green",
|
|
1079
|
-
title: title || "bgrun",
|
|
1080
|
-
titleAlignment: "center",
|
|
1081
|
-
borderStyle: "round"
|
|
1082
|
-
}));
|
|
1083
|
-
}
|
|
1084
|
-
function error(message) {
|
|
1085
|
-
const text = message instanceof Error ? message.stack || message.message : String(message);
|
|
1086
|
-
console.error(boxen(chalk.red(text), {
|
|
1087
|
-
padding: 1,
|
|
1088
|
-
margin: 1,
|
|
1089
|
-
borderColor: "red",
|
|
1090
|
-
title: "Error",
|
|
1091
|
-
titleAlignment: "center",
|
|
1092
|
-
borderStyle: "double"
|
|
1093
|
-
}));
|
|
1094
|
-
throw new BgrunError(text);
|
|
1095
|
-
}
|
|
1096
|
-
var BgrunError;
|
|
1097
|
-
var init_logger = __esm(() => {
|
|
1098
|
-
BgrunError = class BgrunError extends Error {
|
|
1099
|
-
constructor(message) {
|
|
1100
|
-
super(message);
|
|
1101
|
-
this.name = "BgrunError";
|
|
1102
|
-
}
|
|
1103
|
-
};
|
|
1104
|
-
});
|
|
1105
|
-
|
|
1106
|
-
// src/config.ts
|
|
1107
|
-
function formatEnvKey(key) {
|
|
1108
|
-
return key.toUpperCase().replace(/\./g, "_");
|
|
1109
|
-
}
|
|
1110
|
-
function flattenConfig(obj, prefix = "") {
|
|
1111
|
-
return Object.keys(obj).reduce((acc, key) => {
|
|
1112
|
-
const value = obj[key];
|
|
1113
|
-
const newPrefix = prefix ? `${prefix}.${key}` : key;
|
|
1114
|
-
if (Array.isArray(value)) {
|
|
1115
|
-
value.forEach((item, index) => {
|
|
1116
|
-
const indexedPrefix = `${newPrefix}.${index}`;
|
|
1117
|
-
if (typeof item === "object" && item !== null) {
|
|
1118
|
-
Object.assign(acc, flattenConfig(item, indexedPrefix));
|
|
1119
|
-
} else {
|
|
1120
|
-
acc[formatEnvKey(indexedPrefix)] = String(item);
|
|
1121
|
-
}
|
|
1122
|
-
});
|
|
1123
|
-
} else if (typeof value === "object" && value !== null) {
|
|
1124
|
-
Object.assign(acc, flattenConfig(value, newPrefix));
|
|
1125
|
-
} else {
|
|
1126
|
-
acc[formatEnvKey(newPrefix)] = String(value);
|
|
1127
|
-
}
|
|
1128
|
-
return acc;
|
|
1129
|
-
}, {});
|
|
1130
|
-
}
|
|
1131
|
-
async function parseConfigFile(configPath) {
|
|
1132
|
-
const importPath = `${configPath}?t=${Date.now()}`;
|
|
1133
|
-
const parsedConfig = await import(importPath).then((m) => m.default);
|
|
1134
|
-
return flattenConfig(parsedConfig);
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
// src/commands/run.ts
|
|
1138
|
-
var {$: $2 } = globalThis.Bun;
|
|
1139
|
-
var {sleep: sleep2 } = globalThis.Bun;
|
|
1140
|
-
import { join as join3 } from "path";
|
|
1141
|
-
import { createMeasure as createMeasure2 } from "measure-fn";
|
|
1142
|
-
async function handleRun(options) {
|
|
1143
|
-
const { command, directory, env, name, configPath, force, fetch: fetch2, stdout, stderr } = options;
|
|
1144
|
-
const existingProcess = name ? getProcess(name) : null;
|
|
1145
|
-
if (name && existingProcess) {
|
|
1146
|
-
const { getUnmetDeps: getUnmetDeps2 } = await Promise.resolve().then(() => (init_deps(), exports_deps));
|
|
1147
|
-
const unmet = await getUnmetDeps2(name);
|
|
1148
|
-
if (unmet.length > 0) {
|
|
1149
|
-
await run.measure(`Start ${unmet.length} dependencies for "${name}"`, async () => {
|
|
1150
|
-
for (const depName of unmet) {
|
|
1151
|
-
const depProc = getProcess(depName);
|
|
1152
|
-
if (depProc) {
|
|
1153
|
-
announce(`\uD83D\uDCE6 Starting dependency "${depName}" for "${name}"`, "Dependency");
|
|
1154
|
-
await handleRun({ action: "run", name: depName, force: true, remoteName: "" });
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
});
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
if (existingProcess) {
|
|
1161
|
-
const finalDirectory2 = directory || existingProcess.workdir;
|
|
1162
|
-
validateDirectory(finalDirectory2);
|
|
1163
|
-
$2.cwd(finalDirectory2);
|
|
1164
|
-
if (fetch2) {
|
|
1165
|
-
if (!__require("fs").existsSync(__require("path").join(finalDirectory2, ".git"))) {
|
|
1166
|
-
error(`Cannot --fetch: '${finalDirectory2}' is not a Git repository.`);
|
|
1167
|
-
}
|
|
1168
|
-
await run.measure(`Git fetch "${name}"`, async () => {
|
|
1169
|
-
try {
|
|
1170
|
-
await $2`git fetch origin`;
|
|
1171
|
-
const localHash = (await $2`git rev-parse HEAD`.text()).trim();
|
|
1172
|
-
const remoteHash = (await $2`git rev-parse origin/$(git rev-parse --abbrev-ref HEAD)`.text()).trim();
|
|
1173
|
-
if (localHash !== remoteHash) {
|
|
1174
|
-
await $2`git pull origin $(git rev-parse --abbrev-ref HEAD)`;
|
|
1175
|
-
announce("\uD83D\uDCE5 Pulled latest changes", "Git Update");
|
|
1176
|
-
}
|
|
1177
|
-
} catch (err) {
|
|
1178
|
-
error(`Failed to pull latest changes: ${err}`);
|
|
1179
|
-
}
|
|
1180
|
-
});
|
|
1181
|
-
}
|
|
1182
|
-
const isRunning = await isProcessRunning(existingProcess.pid);
|
|
1183
|
-
if (isRunning && !force) {
|
|
1184
|
-
error(`Process '${name}' is currently running. Use --force to restart.`);
|
|
1185
|
-
}
|
|
1186
|
-
let detectedPorts = [];
|
|
1187
|
-
if (isRunning) {
|
|
1188
|
-
detectedPorts = await getProcessPorts(existingProcess.pid);
|
|
1189
|
-
}
|
|
1190
|
-
if (isRunning) {
|
|
1191
|
-
await run.measure(`Terminate "${name}" (PID ${existingProcess.pid})`, async () => {
|
|
1192
|
-
await terminateProcess(existingProcess.pid);
|
|
1193
|
-
announce(`\uD83D\uDD25 Terminated existing process '${name}'`, "Process Terminated");
|
|
1194
|
-
});
|
|
1195
|
-
}
|
|
1196
|
-
if (detectedPorts.length > 0) {
|
|
1197
|
-
await run.measure(`Port cleanup [${detectedPorts.join(", ")}]`, async () => {
|
|
1198
|
-
for (const port of detectedPorts) {
|
|
1199
|
-
await killProcessOnPort(port);
|
|
1200
|
-
}
|
|
1201
|
-
for (const port of detectedPorts) {
|
|
1202
|
-
const freed = await waitForPortFree(port, 5000);
|
|
1203
|
-
if (!freed) {
|
|
1204
|
-
await killProcessOnPort(port);
|
|
1205
|
-
await waitForPortFree(port, 3000);
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
});
|
|
1209
|
-
}
|
|
1210
|
-
const cmdToMatch = existingProcess.command;
|
|
1211
|
-
if (cmdToMatch) {
|
|
1212
|
-
await run.measure("Zombie sweep", async () => {
|
|
1213
|
-
try {
|
|
1214
|
-
const cmdKeyword = cmdToMatch.split(" ")[1] || cmdToMatch;
|
|
1215
|
-
const GENERIC_KEYWORDS = ["dev", "run", "start", "serve", "build", "test"];
|
|
1216
|
-
if (GENERIC_KEYWORDS.includes(cmdKeyword.toLowerCase())) {
|
|
1217
|
-
return;
|
|
1218
|
-
}
|
|
1219
|
-
const currentPid = process.pid;
|
|
1220
|
-
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);
|
|
1221
|
-
const zombiePids = result.split(`
|
|
1222
|
-
`).map((l) => parseInt(l.trim())).filter((n) => !isNaN(n) && n > 0 && n !== currentPid);
|
|
1223
|
-
for (const zPid of zombiePids) {
|
|
1224
|
-
await $2`taskkill /F /T /PID ${zPid}`.nothrow().quiet();
|
|
1225
|
-
}
|
|
1226
|
-
if (zombiePids.length > 0) {
|
|
1227
|
-
announce(`\uD83E\uDDF9 Swept ${zombiePids.length} zombie process(es)`, "Zombie Cleanup");
|
|
1228
|
-
}
|
|
1229
|
-
} catch {}
|
|
1230
|
-
});
|
|
1231
|
-
}
|
|
1232
|
-
await retryDatabaseOperation(() => removeProcessByName(name));
|
|
1233
|
-
} else {
|
|
1234
|
-
if (!directory || !name || !command) {
|
|
1235
|
-
error("'directory', 'name', and 'command' parameters are required for new processes.");
|
|
1236
|
-
}
|
|
1237
|
-
validateDirectory(directory);
|
|
1238
|
-
$2.cwd(directory);
|
|
1239
|
-
}
|
|
1240
|
-
const finalCommand = command || existingProcess.command;
|
|
1241
|
-
const finalDirectory = directory || existingProcess?.workdir;
|
|
1242
|
-
let finalEnv = env || (existingProcess ? parseEnvString(existingProcess.env) : {});
|
|
1243
|
-
if (!("BGR_KEEP_ALIVE" in finalEnv)) {
|
|
1244
|
-
finalEnv.BGR_KEEP_ALIVE = "true";
|
|
1245
|
-
}
|
|
1246
|
-
let finalConfigPath;
|
|
1247
|
-
if (configPath !== undefined) {
|
|
1248
|
-
finalConfigPath = configPath;
|
|
1249
|
-
} else if (existingProcess) {
|
|
1250
|
-
finalConfigPath = existingProcess.configPath;
|
|
1251
|
-
} else {
|
|
1252
|
-
finalConfigPath = ".config.toml";
|
|
1253
|
-
}
|
|
1254
|
-
if (finalConfigPath) {
|
|
1255
|
-
const fullConfigPath = join3(finalDirectory, finalConfigPath);
|
|
1256
|
-
if (await Bun.file(fullConfigPath).exists()) {
|
|
1257
|
-
const configEnv = await run.measure(`Parse config "${finalConfigPath}"`, async () => {
|
|
1258
|
-
try {
|
|
1259
|
-
return await parseConfigFile(fullConfigPath);
|
|
1260
|
-
} catch (err) {
|
|
1261
|
-
console.warn(`Warning: Failed to parse config file ${finalConfigPath}: ${err.message}`);
|
|
1262
|
-
return null;
|
|
1263
|
-
}
|
|
1264
|
-
});
|
|
1265
|
-
if (configEnv) {
|
|
1266
|
-
finalEnv = { ...finalEnv, ...configEnv };
|
|
1267
|
-
console.log(`Loaded config from ${finalConfigPath}`);
|
|
1268
|
-
}
|
|
1269
|
-
} else {
|
|
1270
|
-
console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
const stdoutPath = stdout || existingProcess?.stdout_path || join3(homePath2, ".bgr", `${name}-out.txt`);
|
|
1274
|
-
Bun.write(stdoutPath, "");
|
|
1275
|
-
const stderrPath = stderr || existingProcess?.stderr_path || join3(homePath2, ".bgr", `${name}-err.txt`);
|
|
1276
|
-
Bun.write(stderrPath, "");
|
|
1277
|
-
const actualPid = await run.measure(`Spawn "${name}" \u2192 ${finalCommand}`, async () => {
|
|
1278
|
-
const newProcess = Bun.spawn(getShellCommand(finalCommand), {
|
|
1279
|
-
env: { ...Bun.env, ...finalEnv },
|
|
1280
|
-
cwd: finalDirectory,
|
|
1281
|
-
stdout: Bun.file(stdoutPath),
|
|
1282
|
-
stderr: Bun.file(stderrPath)
|
|
1283
|
-
});
|
|
1284
|
-
newProcess.unref();
|
|
1285
|
-
await sleep2(100);
|
|
1286
|
-
const pid = await findChildPid(newProcess.pid);
|
|
1287
|
-
await sleep2(400);
|
|
1288
|
-
return pid;
|
|
1289
|
-
}) ?? 0;
|
|
1290
|
-
await retryDatabaseOperation(() => insertProcess({
|
|
1291
|
-
pid: actualPid,
|
|
1292
|
-
workdir: finalDirectory,
|
|
1293
|
-
command: finalCommand,
|
|
1294
|
-
name,
|
|
1295
|
-
env: Object.entries(finalEnv).map(([k, v]) => `${k}=${v}`).join(","),
|
|
1296
|
-
configPath: finalConfigPath || "",
|
|
1297
|
-
stdout_path: stdoutPath,
|
|
1298
|
-
stderr_path: stderrPath
|
|
1299
|
-
}));
|
|
1300
|
-
announce(`${existingProcess ? "\uD83D\uDD04 Restarted" : "\uD83D\uDE80 Launched"} process "${name}" with PID ${actualPid}`, "Process Started");
|
|
1301
|
-
}
|
|
1302
|
-
var homePath2, run;
|
|
1303
|
-
var init_run = __esm(() => {
|
|
1304
|
-
init_db();
|
|
1305
|
-
init_platform();
|
|
1306
|
-
init_logger();
|
|
1307
|
-
init_utils();
|
|
1308
|
-
homePath2 = getHomeDir();
|
|
1309
|
-
run = createMeasure2("run");
|
|
1310
|
-
});
|
|
1311
|
-
|
|
1312
1152
|
// src/server.ts
|
|
1313
1153
|
var exports_server = {};
|
|
1314
1154
|
__export(exports_server, {
|
|
@@ -1344,18 +1184,20 @@ async function cleanupPort(port) {
|
|
|
1344
1184
|
async function startServer() {
|
|
1345
1185
|
const { start } = await import("melina");
|
|
1346
1186
|
const appDir = path.join(import.meta.dir, "../dashboard/app");
|
|
1347
|
-
const
|
|
1187
|
+
const rawRequestedPort = process.env.BUN_PORT?.trim();
|
|
1188
|
+
const explicitPort = rawRequestedPort ? parseInt(rawRequestedPort, 10) : null;
|
|
1189
|
+
const hasExplicitPort = explicitPort !== null && !isNaN(explicitPort) && explicitPort > 0;
|
|
1190
|
+
const requestedPort = hasExplicitPort ? explicitPort : 3000;
|
|
1348
1191
|
_originalPort = requestedPort;
|
|
1349
|
-
const resolvedPort = await cleanupPort(requestedPort);
|
|
1192
|
+
const resolvedPort = hasExplicitPort ? await cleanupPort(requestedPort) : requestedPort;
|
|
1350
1193
|
_currentPort = resolvedPort;
|
|
1351
|
-
const needsExplicitPort =
|
|
1194
|
+
const needsExplicitPort = hasExplicitPort || resolvedPort !== requestedPort;
|
|
1352
1195
|
await start({
|
|
1353
1196
|
appDir,
|
|
1354
1197
|
defaultTitle: "bgrun Dashboard - Process Manager",
|
|
1355
1198
|
globalCss: path.join(appDir, "globals.css"),
|
|
1356
1199
|
...needsExplicitPort && { port: resolvedPort }
|
|
1357
1200
|
});
|
|
1358
|
-
startGuard();
|
|
1359
1201
|
const { startLogRotation: startLogRotation2 } = await Promise.resolve().then(() => (init_log_rotation(), exports_log_rotation));
|
|
1360
1202
|
startLogRotation2(() => getAllProcesses());
|
|
1361
1203
|
if (resolvedPort !== requestedPort) {
|
|
@@ -1384,254 +1226,638 @@ function startStickyPortChecker() {
|
|
|
1384
1226
|
} catch {}
|
|
1385
1227
|
}, CHECK_INTERVAL_MS);
|
|
1386
1228
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
setInterval(async () => {
|
|
1390
|
-
try {
|
|
1391
|
-
const processes = getAllProcesses();
|
|
1392
|
-
if (processes.length === 0)
|
|
1393
|
-
return;
|
|
1394
|
-
for (const proc of processes) {
|
|
1395
|
-
if (GUARD_SKIP_NAMES.has(proc.name))
|
|
1396
|
-
continue;
|
|
1397
|
-
const env = proc.env ? parseEnvString(proc.env) : {};
|
|
1398
|
-
if (env.BGR_KEEP_ALIVE !== "true")
|
|
1399
|
-
continue;
|
|
1400
|
-
const alive = await isProcessRunning(proc.pid, proc.command);
|
|
1401
|
-
if (!alive) {
|
|
1402
|
-
const now = Date.now();
|
|
1403
|
-
const nextRestart = guardNextRestartTime.get(proc.name) || 0;
|
|
1404
|
-
if (now < nextRestart)
|
|
1405
|
-
continue;
|
|
1406
|
-
console.log(`[guard] \u26A0 Guarded process "${proc.name}" (PID ${proc.pid}) is dead, restarting...`);
|
|
1407
|
-
let success = false;
|
|
1408
|
-
try {
|
|
1409
|
-
await handleRun({
|
|
1410
|
-
action: "run",
|
|
1411
|
-
name: proc.name,
|
|
1412
|
-
force: true,
|
|
1413
|
-
remoteName: ""
|
|
1414
|
-
});
|
|
1415
|
-
success = true;
|
|
1416
|
-
const prevCount = guardRestartCounts.get(proc.name) || 0;
|
|
1417
|
-
const newCount = prevCount + 1;
|
|
1418
|
-
guardRestartCounts.set(proc.name, newCount);
|
|
1419
|
-
try {
|
|
1420
|
-
addHistoryEntry(proc.name, "restart", undefined, { by: "guard", count: newCount });
|
|
1421
|
-
} catch {}
|
|
1422
|
-
guardEvents.unshift({ time: now, name: proc.name, action: "restart", success: true });
|
|
1423
|
-
if (guardEvents.length > 100)
|
|
1424
|
-
guardEvents.pop();
|
|
1425
|
-
if (newCount > 5) {
|
|
1426
|
-
const backoffSeconds = Math.min(30 * Math.pow(2, newCount - 6), 300);
|
|
1427
|
-
guardNextRestartTime.set(proc.name, Date.now() + backoffSeconds * 1000);
|
|
1428
|
-
console.log(`[guard] \u2713 Restarted "${proc.name}" (restart #${newCount}). Crash loop detected: next check delayed by ${backoffSeconds}s.`);
|
|
1429
|
-
} else {
|
|
1430
|
-
console.log(`[guard] \u2713 Restarted "${proc.name}" (restart #${newCount})`);
|
|
1431
|
-
}
|
|
1432
|
-
} catch (err) {
|
|
1433
|
-
console.error(`[guard] \u2717 Failed to restart "${proc.name}": ${err.message}`);
|
|
1434
|
-
guardEvents.unshift({ time: now, name: proc.name, action: "restart", success: false });
|
|
1435
|
-
if (guardEvents.length > 100)
|
|
1436
|
-
guardEvents.pop();
|
|
1437
|
-
}
|
|
1438
|
-
} else {
|
|
1439
|
-
const prevCount = guardRestartCounts.get(proc.name) || 0;
|
|
1440
|
-
if (prevCount > 0) {
|
|
1441
|
-
const nextRestart = guardNextRestartTime.get(proc.name) || 0;
|
|
1442
|
-
if (Date.now() > nextRestart + 60000) {
|
|
1443
|
-
guardRestartCounts.delete(proc.name);
|
|
1444
|
-
guardNextRestartTime.delete(proc.name);
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
} catch (err) {
|
|
1450
|
-
console.error(`[guard] Error in guard loop: ${err.message}`);
|
|
1451
|
-
}
|
|
1452
|
-
}, GUARD_INTERVAL_MS);
|
|
1453
|
-
}
|
|
1454
|
-
var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES, _g, guardRestartCounts, guardNextRestartTime, guardEvents, _originalPort = 3000, _currentPort = 3000;
|
|
1455
|
-
var init_server = __esm(() => {
|
|
1229
|
+
var guardRestartCounts, guardEvents, _originalPort = 3000, _currentPort = 3000;
|
|
1230
|
+
var init_server = __esm(async () => {
|
|
1456
1231
|
init_db();
|
|
1457
1232
|
init_platform();
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
_g.__bgrGuardRestartCounts = new Map;
|
|
1464
|
-
if (!_g.__bgrGuardNextRestartTime)
|
|
1465
|
-
_g.__bgrGuardNextRestartTime = new Map;
|
|
1466
|
-
if (!_g.__bgrGuardEvents)
|
|
1467
|
-
_g.__bgrGuardEvents = [];
|
|
1468
|
-
guardRestartCounts = _g.__bgrGuardRestartCounts;
|
|
1469
|
-
guardNextRestartTime = _g.__bgrGuardNextRestartTime;
|
|
1470
|
-
guardEvents = _g.__bgrGuardEvents;
|
|
1233
|
+
guardRestartCounts = new Map;
|
|
1234
|
+
guardEvents = [];
|
|
1235
|
+
if (import.meta.main) {
|
|
1236
|
+
await startServer();
|
|
1237
|
+
}
|
|
1471
1238
|
});
|
|
1472
1239
|
|
|
1473
|
-
// src/
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
if (WEBHOOK_SECRET) {
|
|
1494
|
-
const sig = createHmac("sha256", WEBHOOK_SECRET).update(payload).digest("hex");
|
|
1495
|
-
headers["X-BGR-Signature"] = `sha256=${sig}`;
|
|
1496
|
-
}
|
|
1497
|
-
const controller = new AbortController;
|
|
1498
|
-
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
1499
|
-
await fetch(WEBHOOK_URL, {
|
|
1500
|
-
method: "POST",
|
|
1501
|
-
headers,
|
|
1502
|
-
body: payload,
|
|
1503
|
-
signal: controller.signal
|
|
1504
|
-
});
|
|
1505
|
-
clearTimeout(timeout);
|
|
1506
|
-
} catch (err) {
|
|
1507
|
-
console.error(`[guard] Webhook failed: ${err.message}`);
|
|
1508
|
-
}
|
|
1240
|
+
// src/index.ts
|
|
1241
|
+
init_utils();
|
|
1242
|
+
import { parseArgs } from "util";
|
|
1243
|
+
|
|
1244
|
+
// src/commands/run.ts
|
|
1245
|
+
init_db();
|
|
1246
|
+
init_platform();
|
|
1247
|
+
|
|
1248
|
+
// src/logger.ts
|
|
1249
|
+
import boxen from "boxen";
|
|
1250
|
+
import chalk from "chalk";
|
|
1251
|
+
function announce(message, title) {
|
|
1252
|
+
console.log(boxen(message, {
|
|
1253
|
+
padding: 1,
|
|
1254
|
+
margin: 1,
|
|
1255
|
+
borderColor: "green",
|
|
1256
|
+
title: title || "bgrun",
|
|
1257
|
+
titleAlignment: "center",
|
|
1258
|
+
borderStyle: "round"
|
|
1259
|
+
}));
|
|
1509
1260
|
}
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
force: true,
|
|
1516
|
-
remoteName: ""
|
|
1517
|
-
});
|
|
1518
|
-
return true;
|
|
1519
|
-
} catch (err) {
|
|
1520
|
-
console.error(`[guard] \u2717 Failed to restart "${name}": ${err.message}`);
|
|
1521
|
-
return false;
|
|
1261
|
+
|
|
1262
|
+
class BgrunError extends Error {
|
|
1263
|
+
constructor(message) {
|
|
1264
|
+
super(message);
|
|
1265
|
+
this.name = "BgrunError";
|
|
1522
1266
|
}
|
|
1523
1267
|
}
|
|
1524
|
-
function
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1268
|
+
function error(message) {
|
|
1269
|
+
const text = message instanceof Error ? message.stack || message.message : String(message);
|
|
1270
|
+
console.error(boxen(chalk.red(text), {
|
|
1271
|
+
padding: 1,
|
|
1272
|
+
margin: 1,
|
|
1273
|
+
borderColor: "red",
|
|
1274
|
+
title: "Error",
|
|
1275
|
+
titleAlignment: "center",
|
|
1276
|
+
borderStyle: "double"
|
|
1277
|
+
}));
|
|
1278
|
+
throw new BgrunError(text);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// src/commands/run.ts
|
|
1282
|
+
init_utils();
|
|
1283
|
+
|
|
1284
|
+
// src/config.ts
|
|
1285
|
+
function formatEnvKey(key) {
|
|
1286
|
+
return key.toUpperCase().replace(/\./g, "_");
|
|
1287
|
+
}
|
|
1288
|
+
function flattenConfig(obj, prefix = "") {
|
|
1289
|
+
return Object.keys(obj).reduce((acc, key) => {
|
|
1290
|
+
const value = obj[key];
|
|
1291
|
+
const newPrefix = prefix ? `${prefix}.${key}` : key;
|
|
1292
|
+
if (Array.isArray(value)) {
|
|
1293
|
+
value.forEach((item, index) => {
|
|
1294
|
+
const indexedPrefix = `${newPrefix}.${index}`;
|
|
1295
|
+
if (typeof item === "object" && item !== null) {
|
|
1296
|
+
Object.assign(acc, flattenConfig(item, indexedPrefix));
|
|
1297
|
+
} else {
|
|
1298
|
+
acc[formatEnvKey(indexedPrefix)] = String(item);
|
|
1299
|
+
}
|
|
1300
|
+
});
|
|
1301
|
+
} else if (typeof value === "object" && value !== null) {
|
|
1302
|
+
Object.assign(acc, flattenConfig(value, newPrefix));
|
|
1303
|
+
} else {
|
|
1304
|
+
acc[formatEnvKey(newPrefix)] = String(value);
|
|
1305
|
+
}
|
|
1306
|
+
return acc;
|
|
1307
|
+
}, {});
|
|
1308
|
+
}
|
|
1309
|
+
async function parseConfigFile(configPath) {
|
|
1310
|
+
const importPath = `${configPath}?t=${Date.now()}`;
|
|
1311
|
+
const parsedConfig = await import(importPath).then((m) => m.default);
|
|
1312
|
+
return flattenConfig(parsedConfig);
|
|
1313
|
+
}
|
|
1314
|
+
async function loadConfigEnv(directory, configPath = ".config.toml") {
|
|
1315
|
+
const { join: join4 } = await import("path");
|
|
1316
|
+
const fullConfigPath = join4(directory, configPath);
|
|
1317
|
+
if (!await Bun.file(fullConfigPath).exists()) {
|
|
1318
|
+
return {
|
|
1319
|
+
configEnv: {},
|
|
1320
|
+
fullConfigPath,
|
|
1321
|
+
exists: false
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
return {
|
|
1325
|
+
configEnv: await parseConfigFile(fullConfigPath),
|
|
1326
|
+
fullConfigPath,
|
|
1327
|
+
exists: true
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// src/commands/run.ts
|
|
1332
|
+
var {$: $2 } = globalThis.Bun;
|
|
1333
|
+
var {sleep: sleep2 } = globalThis.Bun;
|
|
1334
|
+
import { join as join5 } from "path";
|
|
1335
|
+
import { createMeasure as createMeasure2 } from "measure-fn";
|
|
1336
|
+
|
|
1337
|
+
// src/watcher.ts
|
|
1338
|
+
init_db();
|
|
1339
|
+
init_platform();
|
|
1340
|
+
import { join as join4 } from "path";
|
|
1341
|
+
|
|
1342
|
+
// src/cli-helpers.ts
|
|
1343
|
+
init_db();
|
|
1344
|
+
var MONTH_NAMES = [
|
|
1345
|
+
"january",
|
|
1346
|
+
"february",
|
|
1347
|
+
"march",
|
|
1348
|
+
"april",
|
|
1349
|
+
"may",
|
|
1350
|
+
"june",
|
|
1351
|
+
"july",
|
|
1352
|
+
"august",
|
|
1353
|
+
"september",
|
|
1354
|
+
"october",
|
|
1355
|
+
"november",
|
|
1356
|
+
"december"
|
|
1357
|
+
];
|
|
1358
|
+
var DAY_NAMES = [
|
|
1359
|
+
"",
|
|
1360
|
+
"first",
|
|
1361
|
+
"second",
|
|
1362
|
+
"third",
|
|
1363
|
+
"fourth",
|
|
1364
|
+
"fifth",
|
|
1365
|
+
"sixth",
|
|
1366
|
+
"seventh",
|
|
1367
|
+
"eighth",
|
|
1368
|
+
"ninth",
|
|
1369
|
+
"tenth",
|
|
1370
|
+
"eleventh",
|
|
1371
|
+
"twelfth",
|
|
1372
|
+
"thirteenth",
|
|
1373
|
+
"fourteenth",
|
|
1374
|
+
"fifteenth",
|
|
1375
|
+
"sixteenth",
|
|
1376
|
+
"seventeenth",
|
|
1377
|
+
"eighteenth",
|
|
1378
|
+
"nineteenth",
|
|
1379
|
+
"twentieth",
|
|
1380
|
+
"twenty-first",
|
|
1381
|
+
"twenty-second",
|
|
1382
|
+
"twenty-third",
|
|
1383
|
+
"twenty-fourth",
|
|
1384
|
+
"twenty-fifth",
|
|
1385
|
+
"twenty-sixth",
|
|
1386
|
+
"twenty-seventh",
|
|
1387
|
+
"twenty-eighth",
|
|
1388
|
+
"twenty-ninth",
|
|
1389
|
+
"thirtieth",
|
|
1390
|
+
"thirty-first"
|
|
1391
|
+
];
|
|
1392
|
+
function pad2(value) {
|
|
1393
|
+
return String(value).padStart(2, "0");
|
|
1394
|
+
}
|
|
1395
|
+
function buildDateProcessName(now = new Date) {
|
|
1396
|
+
const month = MONTH_NAMES[now.getMonth()] || "process";
|
|
1397
|
+
const day = DAY_NAMES[now.getDate()] || "day";
|
|
1398
|
+
return `${month}-${day}`;
|
|
1399
|
+
}
|
|
1400
|
+
function generateAutoProcessName(now = new Date) {
|
|
1401
|
+
const baseName = buildDateProcessName(now);
|
|
1402
|
+
if (!getProcess(baseName)) {
|
|
1403
|
+
return baseName;
|
|
1404
|
+
}
|
|
1405
|
+
const timeSuffix = `${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`;
|
|
1406
|
+
const timeName = `${baseName}-${timeSuffix}`;
|
|
1407
|
+
if (!getProcess(timeName)) {
|
|
1408
|
+
return timeName;
|
|
1409
|
+
}
|
|
1410
|
+
for (let i = 1;i < 1000; i++) {
|
|
1411
|
+
const candidate = `${timeName}-${i}`;
|
|
1412
|
+
if (!getProcess(candidate)) {
|
|
1413
|
+
return candidate;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
return `${timeName}-${Date.now()}`;
|
|
1417
|
+
}
|
|
1418
|
+
function shellQuoteArg(arg) {
|
|
1419
|
+
if (process.platform === "win32") {
|
|
1420
|
+
if (/^[A-Za-z0-9_./:\\-]+$/.test(arg))
|
|
1421
|
+
return arg;
|
|
1422
|
+
return `"${arg.replace(/"/g, "\\\"")}"`;
|
|
1423
|
+
}
|
|
1424
|
+
if (/^[A-Za-z0-9_./:-]+$/.test(arg))
|
|
1425
|
+
return arg;
|
|
1426
|
+
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
1427
|
+
}
|
|
1428
|
+
function joinCommandArgs(args) {
|
|
1429
|
+
return args.map(shellQuoteArg).join(" ");
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// src/watcher.ts
|
|
1433
|
+
init_utils();
|
|
1434
|
+
var DEFAULT_INTERVAL_MS = 5000;
|
|
1435
|
+
var CRASH_THRESHOLD = 5;
|
|
1436
|
+
var MAX_BACKOFF_MS = 5 * 60000;
|
|
1437
|
+
var STABILITY_WINDOW_MS = 120000;
|
|
1438
|
+
function getWatcherLogPaths(watcherName) {
|
|
1439
|
+
const homePath2 = getHomeDir();
|
|
1440
|
+
return {
|
|
1441
|
+
stdoutPath: join4(homePath2, ".bgr", `${watcherName}-out.txt`),
|
|
1442
|
+
stderrPath: join4(homePath2, ".bgr", `${watcherName}-err.txt`)
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
async function findDetachedWatcherPid(targetName) {
|
|
1446
|
+
if (process.platform !== "win32")
|
|
1447
|
+
return null;
|
|
1448
|
+
const escaped = targetName.replace(/'/g, "''");
|
|
1449
|
+
const result = await psExec(`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | Where-Object { $_.CommandLine -like '*--_watch-process*' -and $_.CommandLine -like '*${escaped}*' } | Sort-Object -Property CreationDate -Descending | Select-Object -First 1 -ExpandProperty ProcessId`, 4000);
|
|
1450
|
+
const pid = parseInt(result.trim(), 10);
|
|
1451
|
+
return !isNaN(pid) && pid > 0 ? pid : null;
|
|
1452
|
+
}
|
|
1453
|
+
function getInternalWatcherCommand(targetName) {
|
|
1454
|
+
const quotedTarget = shellQuoteArg(targetName);
|
|
1455
|
+
return {
|
|
1456
|
+
storedCommand: `bunx bgrun --_watch-process ${quotedTarget}`,
|
|
1457
|
+
spawnCommand: `bunx bgrun --_watch-process ${quotedTarget}`
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
async function ensureProcessWatcher(targetName) {
|
|
1461
|
+
if (!targetName || isInternalProcessName(targetName))
|
|
1462
|
+
return;
|
|
1463
|
+
const proc = getProcess(targetName);
|
|
1464
|
+
if (!proc)
|
|
1465
|
+
return;
|
|
1466
|
+
const env = parseEnvString(proc.env || "");
|
|
1467
|
+
if (env.BGR_KEEP_ALIVE !== "true") {
|
|
1468
|
+
await stopProcessWatcher(targetName);
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
const watcherName = getWatcherProcessName(targetName);
|
|
1472
|
+
const existingWatcher = getProcess(watcherName);
|
|
1473
|
+
if (existingWatcher && await isProcessRunning(existingWatcher.pid, existingWatcher.command)) {
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
if (existingWatcher) {
|
|
1477
|
+
await retryDatabaseOperation(() => removeProcessByName(watcherName));
|
|
1478
|
+
}
|
|
1479
|
+
const { stdoutPath, stderrPath } = getWatcherLogPaths(watcherName);
|
|
1480
|
+
await Bun.write(stdoutPath, "");
|
|
1481
|
+
await Bun.write(stderrPath, "");
|
|
1482
|
+
const { storedCommand, spawnCommand } = getInternalWatcherCommand(targetName);
|
|
1483
|
+
const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
|
|
1484
|
+
env: {
|
|
1485
|
+
...Bun.env,
|
|
1486
|
+
BGR_STDOUT: stdoutPath,
|
|
1487
|
+
BGR_STDERR: stderrPath
|
|
1488
|
+
},
|
|
1489
|
+
cwd: getHomeDir(),
|
|
1490
|
+
stdout: "ignore",
|
|
1491
|
+
stderr: "ignore",
|
|
1492
|
+
detached: true
|
|
1493
|
+
});
|
|
1494
|
+
newProcess.unref();
|
|
1495
|
+
await Bun.sleep(1000);
|
|
1496
|
+
let actualPid = await findChildPid(newProcess.pid);
|
|
1497
|
+
if (!await isProcessRunning(actualPid)) {
|
|
1498
|
+
const detachedPid = await findDetachedWatcherPid(targetName);
|
|
1499
|
+
if (detachedPid)
|
|
1500
|
+
actualPid = detachedPid;
|
|
1501
|
+
}
|
|
1502
|
+
await retryDatabaseOperation(() => insertProcess({
|
|
1503
|
+
pid: actualPid,
|
|
1504
|
+
workdir: getHomeDir(),
|
|
1505
|
+
command: storedCommand,
|
|
1506
|
+
name: watcherName,
|
|
1507
|
+
env: stringifyEnvString({ BGR_KEEP_ALIVE: "false", BGR_WATCH_TARGET: targetName }),
|
|
1508
|
+
configPath: "",
|
|
1509
|
+
stdout_path: stdoutPath,
|
|
1510
|
+
stderr_path: stderrPath
|
|
1511
|
+
}));
|
|
1512
|
+
}
|
|
1513
|
+
async function stopProcessWatcher(targetName) {
|
|
1514
|
+
const watcherName = getWatcherProcessName(targetName);
|
|
1515
|
+
const watcherProc = getProcess(watcherName);
|
|
1516
|
+
if (!watcherProc)
|
|
1517
|
+
return;
|
|
1518
|
+
if (await isProcessRunning(watcherProc.pid, watcherProc.command)) {
|
|
1519
|
+
await terminateProcess(watcherProc.pid, true);
|
|
1520
|
+
}
|
|
1521
|
+
await retryDatabaseOperation(() => removeProcessByName(watcherName));
|
|
1522
|
+
}
|
|
1523
|
+
async function syncProcessWatcher(targetName, env) {
|
|
1524
|
+
if (!targetName || isInternalProcessName(targetName))
|
|
1525
|
+
return;
|
|
1526
|
+
if (env.BGR_KEEP_ALIVE === "true") {
|
|
1527
|
+
await ensureProcessWatcher(targetName);
|
|
1528
|
+
} else {
|
|
1529
|
+
await stopProcessWatcher(targetName);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
function getBackoffMs(restartCount) {
|
|
1533
|
+
if (restartCount <= CRASH_THRESHOLD)
|
|
1534
|
+
return 0;
|
|
1535
|
+
const exponent = restartCount - CRASH_THRESHOLD;
|
|
1536
|
+
return Math.min(30000 * Math.pow(2, exponent - 1), MAX_BACKOFF_MS);
|
|
1537
|
+
}
|
|
1538
|
+
async function cleanupWatcher(targetName) {
|
|
1539
|
+
const watcherName = getWatcherProcessName(targetName);
|
|
1540
|
+
await retryDatabaseOperation(() => removeProcessByName(watcherName));
|
|
1541
|
+
}
|
|
1542
|
+
async function startProcessWatcher(targetName, intervalMs = DEFAULT_INTERVAL_MS) {
|
|
1543
|
+
const watcherName = getWatcherProcessName(targetName);
|
|
1544
|
+
const releaseWatcherLock = acquireProcessOperationLock(watcherName);
|
|
1545
|
+
let restartCount = 0;
|
|
1546
|
+
let nextRestartAt = 0;
|
|
1547
|
+
let lastSeenAliveAt = 0;
|
|
1531
1548
|
try {
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
if (
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1549
|
+
console.log(`[watcher] Watching "${targetName}" every ${Math.round(intervalMs / 1000)}s`);
|
|
1550
|
+
while (true) {
|
|
1551
|
+
const proc = getProcess(targetName);
|
|
1552
|
+
if (!proc) {
|
|
1553
|
+
console.log(`[watcher] Target "${targetName}" removed; exiting watcher`);
|
|
1554
|
+
break;
|
|
1555
|
+
}
|
|
1556
|
+
const env = parseEnvString(proc.env || "");
|
|
1557
|
+
if (env.BGR_KEEP_ALIVE !== "true") {
|
|
1558
|
+
console.log(`[watcher] Guard disabled for "${targetName}"; exiting watcher`);
|
|
1559
|
+
break;
|
|
1560
|
+
}
|
|
1561
|
+
if (proc.pid <= 0 || isProcessOperationLocked(targetName)) {
|
|
1562
|
+
await Bun.sleep(intervalMs);
|
|
1546
1563
|
continue;
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1564
|
+
}
|
|
1565
|
+
const alive = await isProcessRunning(proc.pid, proc.command);
|
|
1566
|
+
if (!alive) {
|
|
1567
|
+
const now = Date.now();
|
|
1568
|
+
if (now < nextRestartAt) {
|
|
1569
|
+
await Bun.sleep(intervalMs);
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
1572
|
+
try {
|
|
1573
|
+
console.log(`[watcher] Restarting "${targetName}" after detected crash`);
|
|
1574
|
+
await handleRun({
|
|
1575
|
+
action: "run",
|
|
1576
|
+
name: targetName,
|
|
1577
|
+
force: true,
|
|
1578
|
+
remoteName: ""
|
|
1579
|
+
});
|
|
1580
|
+
restartCount++;
|
|
1581
|
+
const backoffMs = getBackoffMs(restartCount);
|
|
1582
|
+
nextRestartAt = backoffMs > 0 ? now + backoffMs : 0;
|
|
1583
|
+
lastSeenAliveAt = 0;
|
|
1584
|
+
addHistoryEntry(targetName, "guard_restart", proc.pid, { by: watcherName, count: restartCount, backoffMs });
|
|
1585
|
+
} catch (err) {
|
|
1586
|
+
addHistoryEntry(targetName, "guard_restart_failed", proc.pid, { by: watcherName, error: err?.message || String(err) });
|
|
1587
|
+
console.error(`[watcher] Failed to restart "${targetName}": ${err.message}`);
|
|
1588
|
+
}
|
|
1589
|
+
} else if (restartCount > 0) {
|
|
1590
|
+
const now = Date.now();
|
|
1591
|
+
if (!lastSeenAliveAt) {
|
|
1592
|
+
lastSeenAliveAt = now;
|
|
1593
|
+
} else if (now - lastSeenAliveAt >= STABILITY_WINDOW_MS) {
|
|
1594
|
+
restartCount = 0;
|
|
1595
|
+
nextRestartAt = 0;
|
|
1596
|
+
lastSeenAliveAt = 0;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
await Bun.sleep(intervalMs);
|
|
1600
|
+
}
|
|
1601
|
+
} finally {
|
|
1602
|
+
releaseWatcherLock();
|
|
1603
|
+
await cleanupWatcher(targetName);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
function getGuardRestartCounts() {
|
|
1607
|
+
const counts = new Map;
|
|
1608
|
+
const entries = db.history.select().where({ event: "guard_restart" }).all();
|
|
1609
|
+
for (const entry of entries) {
|
|
1610
|
+
counts.set(entry.process_name, (counts.get(entry.process_name) || 0) + 1);
|
|
1611
|
+
}
|
|
1612
|
+
return counts;
|
|
1613
|
+
}
|
|
1614
|
+
function getRecentGuardEvents(limit = 100) {
|
|
1615
|
+
const rows = db.history.select().orderBy("timestamp", "desc").limit(limit * 4).all().filter((row) => row.event === "guard_restart" || row.event === "guard_restart_failed").slice(0, limit);
|
|
1616
|
+
return rows.map((row) => ({
|
|
1617
|
+
time: new Date(row.timestamp).getTime(),
|
|
1618
|
+
name: row.process_name,
|
|
1619
|
+
action: "restart",
|
|
1620
|
+
success: row.event === "guard_restart"
|
|
1621
|
+
}));
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// src/commands/run.ts
|
|
1625
|
+
var homePath2 = getHomeDir();
|
|
1626
|
+
var run = createMeasure2("run");
|
|
1627
|
+
var INTERNAL_BUNX_PREFIX = "bunx bgrun";
|
|
1628
|
+
function resolveInternalBgrunCommand(command) {
|
|
1629
|
+
const trimmed = command.trim();
|
|
1630
|
+
if (!trimmed.startsWith("bgrun --_") && !trimmed.startsWith(`${INTERNAL_BUNX_PREFIX} --_`)) {
|
|
1631
|
+
return command;
|
|
1632
|
+
}
|
|
1633
|
+
if (trimmed.startsWith(`${INTERNAL_BUNX_PREFIX} --_`)) {
|
|
1634
|
+
return trimmed;
|
|
1635
|
+
}
|
|
1636
|
+
return `${INTERNAL_BUNX_PREFIX}${trimmed.slice("bgrun".length)}`;
|
|
1637
|
+
}
|
|
1638
|
+
async function handleRun(options) {
|
|
1639
|
+
const {
|
|
1640
|
+
command,
|
|
1641
|
+
directory,
|
|
1642
|
+
env,
|
|
1643
|
+
name,
|
|
1644
|
+
configPath,
|
|
1645
|
+
force,
|
|
1646
|
+
fetch,
|
|
1647
|
+
stdout,
|
|
1648
|
+
stderr
|
|
1649
|
+
} = options;
|
|
1650
|
+
const releaseOperationLock = name ? acquireProcessOperationLock(name) : () => {};
|
|
1651
|
+
try {
|
|
1652
|
+
const existingProcess = name ? getProcess(name) : null;
|
|
1653
|
+
if (name && existingProcess) {
|
|
1654
|
+
const { getUnmetDeps: getUnmetDeps2 } = await Promise.resolve().then(() => (init_deps(), exports_deps));
|
|
1655
|
+
const unmet = await getUnmetDeps2(name);
|
|
1656
|
+
if (unmet.length > 0) {
|
|
1657
|
+
await run.measure(`Start ${unmet.length} dependencies for "${name}"`, async () => {
|
|
1658
|
+
for (const depName of unmet) {
|
|
1659
|
+
const depProc = getProcess(depName);
|
|
1660
|
+
if (depProc) {
|
|
1661
|
+
announce(`\uD83D\uDCE6 Starting dependency "${depName}" for "${name}"`, "Dependency");
|
|
1662
|
+
await handleRun({
|
|
1663
|
+
action: "run",
|
|
1664
|
+
name: depName,
|
|
1665
|
+
force: true,
|
|
1666
|
+
remoteName: ""
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1556
1669
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
if (existingProcess) {
|
|
1674
|
+
const finalDirectory2 = directory || existingProcess.workdir;
|
|
1675
|
+
validateDirectory(finalDirectory2);
|
|
1676
|
+
$2.cwd(finalDirectory2);
|
|
1677
|
+
if (fetch) {
|
|
1678
|
+
if (!__require("fs").existsSync(__require("path").join(finalDirectory2, ".git"))) {
|
|
1679
|
+
error(`Cannot --fetch: '${finalDirectory2}' is not a Git repository.`);
|
|
1680
|
+
}
|
|
1681
|
+
await run.measure(`Git fetch "${name}"`, async () => {
|
|
1682
|
+
try {
|
|
1683
|
+
await $2`git fetch origin`;
|
|
1684
|
+
const localHash = (await $2`git rev-parse HEAD`.text()).trim();
|
|
1685
|
+
const remoteHash = (await $2`git rev-parse origin/$(git rev-parse --abbrev-ref HEAD)`.text()).trim();
|
|
1686
|
+
if (localHash !== remoteHash) {
|
|
1687
|
+
await $2`git pull origin $(git rev-parse --abbrev-ref HEAD)`;
|
|
1688
|
+
announce("\uD83D\uDCE5 Pulled latest changes", "Git Update");
|
|
1570
1689
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1690
|
+
} catch (err) {
|
|
1691
|
+
error(`Failed to pull latest changes: ${err}`);
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
const isRunning = await isProcessRunning(existingProcess.pid);
|
|
1696
|
+
if (isRunning && !force) {
|
|
1697
|
+
error(`Process '${name}' is currently running. Use --force to restart.`);
|
|
1698
|
+
}
|
|
1699
|
+
let actualPid2 = existingProcess.pid;
|
|
1700
|
+
if (!isRunning && !force) {
|
|
1701
|
+
const reconciled = await reconcileProcessPids([
|
|
1702
|
+
{
|
|
1703
|
+
name,
|
|
1704
|
+
pid: existingProcess.pid,
|
|
1705
|
+
command: existingProcess.command,
|
|
1706
|
+
workdir: existingProcess.workdir
|
|
1575
1707
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1708
|
+
], new Set([existingProcess.pid]));
|
|
1709
|
+
const newPid = reconciled.get(name);
|
|
1710
|
+
if (newPid) {
|
|
1711
|
+
console.log(`[run] Reconciled dead PID ${existingProcess.pid} to live PID ${newPid}`);
|
|
1712
|
+
actualPid2 = newPid;
|
|
1713
|
+
updateProcessPid(name, newPid);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
let detectedPorts = [];
|
|
1717
|
+
const actuallyRunning = await isProcessRunning(actualPid2);
|
|
1718
|
+
if (actuallyRunning) {
|
|
1719
|
+
detectedPorts = await getProcessPorts(actualPid2);
|
|
1720
|
+
}
|
|
1721
|
+
if (actuallyRunning) {
|
|
1722
|
+
await run.measure(`Terminate "${name}" (PID ${actualPid2})`, async () => {
|
|
1723
|
+
await terminateProcess(actualPid2);
|
|
1724
|
+
announce(`\uD83D\uDD25 Terminated existing process '${name}'`, "Process Terminated");
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
if (detectedPorts.length > 0) {
|
|
1728
|
+
await run.measure(`Port cleanup [${detectedPorts.join(", ")}]`, async () => {
|
|
1729
|
+
for (const port of detectedPorts) {
|
|
1730
|
+
await killProcessOnPort(port);
|
|
1731
|
+
}
|
|
1732
|
+
for (const port of detectedPorts) {
|
|
1733
|
+
const freed = await waitForPortFree(port, 5000);
|
|
1734
|
+
if (!freed) {
|
|
1735
|
+
await killProcessOnPort(port);
|
|
1736
|
+
await waitForPortFree(port, 3000);
|
|
1587
1737
|
}
|
|
1588
1738
|
}
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
const existingEnv = existingProcess.env ? parseEnvString(existingProcess.env) : {};
|
|
1742
|
+
const declaredPort = getDeclaredPort(existingEnv, existingProcess.command);
|
|
1743
|
+
if (declaredPort && !detectedPorts.includes(declaredPort)) {
|
|
1744
|
+
await run.measure(`Declared port cleanup [${declaredPort}]`, async () => {
|
|
1745
|
+
const portFree = await isPortFree(declaredPort);
|
|
1746
|
+
if (!portFree) {
|
|
1747
|
+
console.log(`[run] Declared port ${declaredPort} is busy (orphaned process), cleaning up...`);
|
|
1748
|
+
await killProcessOnPort(declaredPort);
|
|
1749
|
+
const freed = await waitForPortFree(declaredPort, 5000);
|
|
1750
|
+
if (!freed) {
|
|
1751
|
+
console.warn(`[run] Port ${declaredPort} still busy after cleanup, retrying...`);
|
|
1752
|
+
await killProcessOnPort(declaredPort);
|
|
1753
|
+
await waitForPortFree(declaredPort, 3000);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
const cmdToMatch = existingProcess.command;
|
|
1759
|
+
if (cmdToMatch) {
|
|
1760
|
+
await run.measure("Zombie sweep", async () => {
|
|
1761
|
+
try {
|
|
1762
|
+
const cmdKeyword = cmdToMatch.split(" ")[1] || cmdToMatch;
|
|
1763
|
+
const GENERIC_KEYWORDS = [
|
|
1764
|
+
"dev",
|
|
1765
|
+
"run",
|
|
1766
|
+
"start",
|
|
1767
|
+
"serve",
|
|
1768
|
+
"build",
|
|
1769
|
+
"test"
|
|
1770
|
+
];
|
|
1771
|
+
if (GENERIC_KEYWORDS.includes(cmdKeyword.toLowerCase())) {
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
const currentPid = process.pid;
|
|
1775
|
+
const result = await psExec(`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | Where-Object { $_.CommandLine -like '*${cmdKeyword.replace(/'/g, "''").replace(/([&[\](){}^$|\\*?+])/g, "$&")}*' -and $_.ProcessId -ne ${currentPid} } | Select-Object -ExpandProperty ProcessId`, 3000);
|
|
1776
|
+
const zombiePids = result.split(`
|
|
1777
|
+
`).map((l) => parseInt(l.trim())).filter((n) => !isNaN(n) && n > 0 && n !== currentPid);
|
|
1778
|
+
for (const zPid of zombiePids) {
|
|
1779
|
+
await $2`taskkill /F /T /PID ${zPid}`.nothrow().quiet();
|
|
1780
|
+
}
|
|
1781
|
+
if (zombiePids.length > 0) {
|
|
1782
|
+
announce(`\uD83E\uDDF9 Swept ${zombiePids.length} zombie process(es)`, "Zombie Cleanup");
|
|
1783
|
+
}
|
|
1784
|
+
} catch {}
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
await retryDatabaseOperation(() => removeProcessByName(name));
|
|
1788
|
+
} else {
|
|
1789
|
+
if (!directory || !name || !command) {
|
|
1790
|
+
error("'directory', 'name', and 'command' parameters are required for new processes.");
|
|
1791
|
+
}
|
|
1792
|
+
validateDirectory(directory);
|
|
1793
|
+
$2.cwd(directory);
|
|
1794
|
+
}
|
|
1795
|
+
const storedCommand = command || existingProcess.command;
|
|
1796
|
+
const finalCommand = resolveInternalBgrunCommand(storedCommand);
|
|
1797
|
+
const finalDirectory = directory || existingProcess?.workdir;
|
|
1798
|
+
let finalEnv = env || (existingProcess ? parseEnvString(existingProcess.env) : {});
|
|
1799
|
+
let finalConfigPath;
|
|
1800
|
+
if (configPath !== undefined) {
|
|
1801
|
+
finalConfigPath = configPath;
|
|
1802
|
+
} else if (existingProcess) {
|
|
1803
|
+
finalConfigPath = existingProcess.configPath;
|
|
1804
|
+
} else {
|
|
1805
|
+
finalConfigPath = ".config.toml";
|
|
1806
|
+
}
|
|
1807
|
+
if (finalConfigPath) {
|
|
1808
|
+
const fullConfigPath = join5(finalDirectory, finalConfigPath);
|
|
1809
|
+
if (await Bun.file(fullConfigPath).exists()) {
|
|
1810
|
+
const configEnv = await run.measure(`Parse config "${finalConfigPath}"`, async () => {
|
|
1811
|
+
try {
|
|
1812
|
+
return await parseConfigFile(fullConfigPath);
|
|
1813
|
+
} catch (err) {
|
|
1814
|
+
console.warn(`Warning: Failed to parse config file ${finalConfigPath}: ${err.message}`);
|
|
1815
|
+
return null;
|
|
1816
|
+
}
|
|
1817
|
+
});
|
|
1818
|
+
if (configEnv) {
|
|
1819
|
+
finalEnv = { ...finalEnv, ...configEnv };
|
|
1820
|
+
console.log(`Loaded config from ${finalConfigPath}`);
|
|
1589
1821
|
}
|
|
1590
|
-
}
|
|
1591
|
-
console.
|
|
1822
|
+
} else {
|
|
1823
|
+
console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
|
|
1592
1824
|
}
|
|
1593
1825
|
}
|
|
1594
|
-
|
|
1595
|
-
|
|
1826
|
+
const stdoutPath = stdout || existingProcess?.stdout_path || join5(homePath2, ".bgr", `${name}-out.txt`);
|
|
1827
|
+
Bun.write(stdoutPath, "");
|
|
1828
|
+
const stderrPath = stderr || existingProcess?.stderr_path || join5(homePath2, ".bgr", `${name}-err.txt`);
|
|
1829
|
+
Bun.write(stderrPath, "");
|
|
1830
|
+
const actualPid = await run.measure(`Spawn "${name}" \u2192 ${finalCommand}`, async () => {
|
|
1831
|
+
const newProcess = Bun.spawn(getShellCommand(finalCommand), {
|
|
1832
|
+
env: buildManagedProcessEnv(Bun.env, finalEnv),
|
|
1833
|
+
cwd: finalDirectory,
|
|
1834
|
+
stdout: Bun.file(stdoutPath),
|
|
1835
|
+
stderr: Bun.file(stderrPath)
|
|
1836
|
+
});
|
|
1837
|
+
newProcess.unref();
|
|
1838
|
+
await sleep2(100);
|
|
1839
|
+
const pid = await findChildPid(newProcess.pid);
|
|
1840
|
+
await sleep2(400);
|
|
1841
|
+
return pid;
|
|
1842
|
+
}) ?? 0;
|
|
1843
|
+
await retryDatabaseOperation(() => insertProcess({
|
|
1844
|
+
pid: actualPid,
|
|
1845
|
+
workdir: finalDirectory,
|
|
1846
|
+
command: finalCommand,
|
|
1847
|
+
name,
|
|
1848
|
+
env: stringifyEnvString(finalEnv),
|
|
1849
|
+
configPath: finalConfigPath || "",
|
|
1850
|
+
stdout_path: stdoutPath,
|
|
1851
|
+
stderr_path: stderrPath
|
|
1852
|
+
}));
|
|
1853
|
+
if (!isInternalProcessName(name)) {
|
|
1854
|
+
await syncProcessWatcher(name, finalEnv);
|
|
1596
1855
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1856
|
+
announce(`${existingProcess ? "\uD83D\uDD04 Restarted" : "\uD83D\uDE80 Launched"} process "${name}" with PID ${actualPid}`, "Process Started");
|
|
1857
|
+
} finally {
|
|
1858
|
+
releaseOperationLock();
|
|
1599
1859
|
}
|
|
1600
1860
|
}
|
|
1601
|
-
async function startGuardLoop(intervalMs = DEFAULT_INTERVAL_MS) {
|
|
1602
|
-
const interval = intervalMs || DEFAULT_INTERVAL_MS;
|
|
1603
|
-
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`);
|
|
1604
|
-
console.log(`[guard] \uD83D\uDEE1\uFE0F BGR Standalone Guard started`);
|
|
1605
|
-
console.log(`[guard] Check interval: ${interval / 1000}s`);
|
|
1606
|
-
console.log(`[guard] Crash backoff threshold: ${CRASH_THRESHOLD} restarts`);
|
|
1607
|
-
console.log(`[guard] Stability window: ${STABILITY_WINDOW_MS / 1000}s`);
|
|
1608
|
-
console.log(`[guard] Monitoring: BGR_KEEP_ALIVE=true + bgr-dashboard`);
|
|
1609
|
-
console.log(`[guard] Webhook: ${WEBHOOK_URL || "(none \u2014 set BGR_WEBHOOK_URL to enable)"}`);
|
|
1610
|
-
console.log(`[guard] Started: ${new Date().toLocaleString()}`);
|
|
1611
|
-
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`);
|
|
1612
|
-
await guardCycle();
|
|
1613
|
-
setInterval(guardCycle, interval);
|
|
1614
|
-
}
|
|
1615
|
-
var WEBHOOK_URL, WEBHOOK_SECRET, DEFAULT_INTERVAL_MS = 30000, MAX_BACKOFF_MS, CRASH_THRESHOLD = 5, STABILITY_WINDOW_MS = 120000, state;
|
|
1616
|
-
var init_guard = __esm(() => {
|
|
1617
|
-
init_db();
|
|
1618
|
-
init_platform();
|
|
1619
|
-
init_run();
|
|
1620
|
-
init_utils();
|
|
1621
|
-
WEBHOOK_URL = process.env.BGR_WEBHOOK_URL || "";
|
|
1622
|
-
WEBHOOK_SECRET = process.env.BGR_WEBHOOK_SECRET || "";
|
|
1623
|
-
MAX_BACKOFF_MS = 5 * 60000;
|
|
1624
|
-
state = {
|
|
1625
|
-
restartCounts: new Map,
|
|
1626
|
-
nextRestartTime: new Map,
|
|
1627
|
-
lastSeenAlive: new Map
|
|
1628
|
-
};
|
|
1629
|
-
});
|
|
1630
|
-
|
|
1631
|
-
// src/index.ts
|
|
1632
|
-
init_utils();
|
|
1633
|
-
init_run();
|
|
1634
|
-
import { parseArgs } from "util";
|
|
1635
1861
|
|
|
1636
1862
|
// src/commands/list.ts
|
|
1637
1863
|
import chalk3 from "chalk";
|
|
@@ -1801,7 +2027,6 @@ function renderProcessTable(processes, options) {
|
|
|
1801
2027
|
|
|
1802
2028
|
// src/commands/list.ts
|
|
1803
2029
|
init_db();
|
|
1804
|
-
init_logger();
|
|
1805
2030
|
init_utils();
|
|
1806
2031
|
init_platform();
|
|
1807
2032
|
function formatMemory(bytes) {
|
|
@@ -1815,6 +2040,8 @@ function formatMemory(bytes) {
|
|
|
1815
2040
|
async function showAll(opts) {
|
|
1816
2041
|
const processes = getAllProcesses();
|
|
1817
2042
|
const filtered = processes.filter((proc) => {
|
|
2043
|
+
if (isInternalProcessName(proc.name))
|
|
2044
|
+
return false;
|
|
1818
2045
|
if (!opts?.filter)
|
|
1819
2046
|
return true;
|
|
1820
2047
|
const envVars = parseEnvString(proc.env);
|
|
@@ -1862,11 +2089,21 @@ async function showAll(opts) {
|
|
|
1862
2089
|
for (const proc of filtered) {
|
|
1863
2090
|
const isRunning = aliveCache.get(proc.pid) ?? await isProcessRunning(proc.pid, proc.command);
|
|
1864
2091
|
const runtime = calculateRuntime(proc.timestamp);
|
|
1865
|
-
|
|
1866
|
-
|
|
2092
|
+
let displayPid = proc.pid;
|
|
2093
|
+
let ports = [];
|
|
2094
|
+
if (isRunning) {
|
|
2095
|
+
const resolved = await resolvePidWithPorts(proc.pid);
|
|
2096
|
+
displayPid = resolved.pid;
|
|
2097
|
+
ports = resolved.ports;
|
|
2098
|
+
if (displayPid !== proc.pid) {
|
|
2099
|
+
updateProcessPid(proc.name, displayPid);
|
|
2100
|
+
proc.pid = displayPid;
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
const mem = isRunning ? resourceMap.get(displayPid)?.memory || resourceMap.get(proc.pid)?.memory || 0 : 0;
|
|
1867
2104
|
tableData.push({
|
|
1868
2105
|
id: proc.id,
|
|
1869
|
-
pid:
|
|
2106
|
+
pid: displayPid,
|
|
1870
2107
|
name: proc.name,
|
|
1871
2108
|
port: ports.length > 0 ? ports.map((p) => `:${p}`).join(",") : "-",
|
|
1872
2109
|
memory: formatMemory(mem),
|
|
@@ -1898,7 +2135,7 @@ async function showAll(opts) {
|
|
|
1898
2135
|
// src/commands/cleanup.ts
|
|
1899
2136
|
init_db();
|
|
1900
2137
|
init_platform();
|
|
1901
|
-
|
|
2138
|
+
init_utils();
|
|
1902
2139
|
import * as fs3 from "fs";
|
|
1903
2140
|
async function handleDelete(name) {
|
|
1904
2141
|
const process2 = getProcess(name);
|
|
@@ -1910,6 +2147,9 @@ async function handleDelete(name) {
|
|
|
1910
2147
|
if (isRunning) {
|
|
1911
2148
|
await terminateProcess(process2.pid);
|
|
1912
2149
|
}
|
|
2150
|
+
if (!isInternalProcessName(name)) {
|
|
2151
|
+
await stopProcessWatcher(name);
|
|
2152
|
+
}
|
|
1913
2153
|
if (fs3.existsSync(process2.stdout_path)) {
|
|
1914
2154
|
try {
|
|
1915
2155
|
fs3.unlinkSync(process2.stdout_path);
|
|
@@ -1930,6 +2170,12 @@ async function handleClean() {
|
|
|
1930
2170
|
for (const proc of processes) {
|
|
1931
2171
|
const running = await isProcessRunning(proc.pid);
|
|
1932
2172
|
if (!running) {
|
|
2173
|
+
const watched = getWatchedProcessName(proc.name);
|
|
2174
|
+
if (watched) {
|
|
2175
|
+
removeProcess(proc.pid);
|
|
2176
|
+
cleanedCount++;
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
1933
2179
|
removeProcess(proc.pid);
|
|
1934
2180
|
cleanedCount++;
|
|
1935
2181
|
if (fs3.existsSync(proc.stdout_path)) {
|
|
@@ -1958,18 +2204,33 @@ async function handleStop(name) {
|
|
|
1958
2204
|
error(`No process found named '${name}'`);
|
|
1959
2205
|
return;
|
|
1960
2206
|
}
|
|
1961
|
-
const
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
await
|
|
2207
|
+
const releaseOperationLock = acquireProcessOperationLock(name);
|
|
2208
|
+
try {
|
|
2209
|
+
const isRunning = await isProcessRunning(proc.pid);
|
|
2210
|
+
if (!isRunning) {
|
|
2211
|
+
announce(`Process '${name}' is already stopped.`, "Process Stop");
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
const ports = await getProcessPorts(proc.pid);
|
|
2215
|
+
await terminateProcess(proc.pid);
|
|
2216
|
+
for (const port of ports) {
|
|
2217
|
+
await killProcessOnPort(port);
|
|
2218
|
+
}
|
|
2219
|
+
const procEnv = proc.env ? parseEnvString(proc.env) : {};
|
|
2220
|
+
const declaredPort = getDeclaredPort(procEnv, proc.command);
|
|
2221
|
+
if (declaredPort && !ports.includes(declaredPort)) {
|
|
2222
|
+
const portFree = await isPortFree(declaredPort);
|
|
2223
|
+
if (!portFree) {
|
|
2224
|
+
console.log(`[stop] Declared port ${declaredPort} is busy (orphaned process), cleaning up...`);
|
|
2225
|
+
await killProcessOnPort(declaredPort);
|
|
2226
|
+
await waitForPortFree(declaredPort, 3000);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
updateProcessPid(name, 0);
|
|
2230
|
+
announce(`Process '${name}' has been stopped (kept in registry).`, "Process Stopped");
|
|
2231
|
+
} finally {
|
|
2232
|
+
releaseOperationLock();
|
|
1970
2233
|
}
|
|
1971
|
-
updateProcessPid(name, 0);
|
|
1972
|
-
announce(`Process '${name}' has been stopped (kept in registry).`, "Process Stopped");
|
|
1973
2234
|
}
|
|
1974
2235
|
async function handleDeleteAll() {
|
|
1975
2236
|
const processes = getAllProcesses();
|
|
@@ -1980,6 +2241,9 @@ async function handleDeleteAll() {
|
|
|
1980
2241
|
let killedCount = 0;
|
|
1981
2242
|
let portsFreed = 0;
|
|
1982
2243
|
for (const proc of processes) {
|
|
2244
|
+
if (!isInternalProcessName(proc.name)) {
|
|
2245
|
+
await stopProcessWatcher(proc.name);
|
|
2246
|
+
}
|
|
1983
2247
|
const running = await isProcessRunning(proc.pid);
|
|
1984
2248
|
if (running) {
|
|
1985
2249
|
const ports = await getProcessPorts(proc.pid);
|
|
@@ -1995,6 +2259,18 @@ async function handleDeleteAll() {
|
|
|
1995
2259
|
portsFreed++;
|
|
1996
2260
|
}
|
|
1997
2261
|
}
|
|
2262
|
+
const procEnv = proc.env ? parseEnvString(proc.env) : {};
|
|
2263
|
+
const declaredPort = getDeclaredPort(procEnv, proc.command);
|
|
2264
|
+
if (declaredPort) {
|
|
2265
|
+
const portFree = await isPortFree(declaredPort);
|
|
2266
|
+
if (!portFree) {
|
|
2267
|
+
console.log(`[nuke] Declared port ${declaredPort} is busy, cleaning up...`);
|
|
2268
|
+
await killProcessOnPort(declaredPort);
|
|
2269
|
+
const freed = await waitForPortFree(declaredPort, 3000);
|
|
2270
|
+
if (freed)
|
|
2271
|
+
portsFreed++;
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
1998
2274
|
if (fs3.existsSync(proc.stdout_path)) {
|
|
1999
2275
|
try {
|
|
2000
2276
|
fs3.unlinkSync(proc.stdout_path);
|
|
@@ -2018,9 +2294,7 @@ async function handleDeleteAll() {
|
|
|
2018
2294
|
// src/commands/watch.ts
|
|
2019
2295
|
init_db();
|
|
2020
2296
|
init_platform();
|
|
2021
|
-
init_logger();
|
|
2022
2297
|
init_utils();
|
|
2023
|
-
init_run();
|
|
2024
2298
|
import * as fs4 from "fs";
|
|
2025
2299
|
import path2 from "path";
|
|
2026
2300
|
import chalk4 from "chalk";
|
|
@@ -2210,7 +2484,6 @@ SIGINT received...`));
|
|
|
2210
2484
|
|
|
2211
2485
|
// src/commands/logs.ts
|
|
2212
2486
|
init_db();
|
|
2213
|
-
init_logger();
|
|
2214
2487
|
init_platform();
|
|
2215
2488
|
import chalk5 from "chalk";
|
|
2216
2489
|
import * as fs5 from "fs";
|
|
@@ -2255,7 +2528,6 @@ async function showLogs(name, logType = "both", lines) {
|
|
|
2255
2528
|
}
|
|
2256
2529
|
|
|
2257
2530
|
// src/commands/details.ts
|
|
2258
|
-
init_logger();
|
|
2259
2531
|
init_db();
|
|
2260
2532
|
init_utils();
|
|
2261
2533
|
init_platform();
|
|
@@ -2266,6 +2538,10 @@ async function showDetails(name) {
|
|
|
2266
2538
|
error(`No process found named '${name}'`);
|
|
2267
2539
|
return;
|
|
2268
2540
|
}
|
|
2541
|
+
if (isInternalProcessName(proc.name)) {
|
|
2542
|
+
error(`'${name}' is an internal bgrun process.`);
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
2269
2545
|
let isRunning = await isProcessRunning(proc.pid, proc.command);
|
|
2270
2546
|
if (!isRunning && proc.pid > 0) {
|
|
2271
2547
|
const reconciled = await reconcileProcessPids([{ name: proc.name, pid: proc.pid, command: proc.command, workdir: proc.workdir }], new Set([proc.pid]));
|
|
@@ -2278,7 +2554,15 @@ async function showDetails(name) {
|
|
|
2278
2554
|
}
|
|
2279
2555
|
const runtime = calculateRuntime(proc.timestamp);
|
|
2280
2556
|
const envVars = parseEnvString(proc.env);
|
|
2281
|
-
|
|
2557
|
+
let ports = [];
|
|
2558
|
+
if (isRunning) {
|
|
2559
|
+
const resolved = await resolvePidWithPorts(proc.pid);
|
|
2560
|
+
ports = resolved.ports;
|
|
2561
|
+
if (resolved.pid !== proc.pid) {
|
|
2562
|
+
updateProcessPid(proc.name, resolved.pid);
|
|
2563
|
+
proc.pid = resolved.pid;
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2282
2566
|
const portDisplay = ports.length > 0 ? ports.map((p) => chalk6.hex("#FF6B6B")(`:${p}`)).join(", ") : null;
|
|
2283
2567
|
const details = `
|
|
2284
2568
|
${chalk6.bold("Process Details:")}
|
|
@@ -2302,13 +2586,225 @@ ${Object.entries(envVars).map(([key, value]) => `${chalk6.cyan.bold(key)} = ${ch
|
|
|
2302
2586
|
announce(details, `Process Details: ${name}`);
|
|
2303
2587
|
}
|
|
2304
2588
|
|
|
2589
|
+
// src/commands/envit.ts
|
|
2590
|
+
function parseEnvitArgs(args) {
|
|
2591
|
+
let directory;
|
|
2592
|
+
let configPath;
|
|
2593
|
+
let shell;
|
|
2594
|
+
let help = false;
|
|
2595
|
+
for (let i = 0;i < args.length; i++) {
|
|
2596
|
+
const arg = args[i];
|
|
2597
|
+
if (arg === "--help" || arg === "-h") {
|
|
2598
|
+
help = true;
|
|
2599
|
+
continue;
|
|
2600
|
+
}
|
|
2601
|
+
if (arg === "--directory") {
|
|
2602
|
+
directory = args[++i];
|
|
2603
|
+
if (!directory)
|
|
2604
|
+
error("Missing value for --directory.");
|
|
2605
|
+
continue;
|
|
2606
|
+
}
|
|
2607
|
+
if (arg.startsWith("--directory=")) {
|
|
2608
|
+
directory = arg.slice("--directory=".length);
|
|
2609
|
+
if (!directory)
|
|
2610
|
+
error("Missing value for --directory.");
|
|
2611
|
+
continue;
|
|
2612
|
+
}
|
|
2613
|
+
if (arg === "--config") {
|
|
2614
|
+
configPath = args[++i];
|
|
2615
|
+
if (!configPath)
|
|
2616
|
+
error("Missing value for --config.");
|
|
2617
|
+
continue;
|
|
2618
|
+
}
|
|
2619
|
+
if (arg.startsWith("--config=")) {
|
|
2620
|
+
configPath = arg.slice("--config=".length);
|
|
2621
|
+
if (!configPath)
|
|
2622
|
+
error("Missing value for --config.");
|
|
2623
|
+
continue;
|
|
2624
|
+
}
|
|
2625
|
+
if (arg === "--shell") {
|
|
2626
|
+
const value = args[++i];
|
|
2627
|
+
if (!value)
|
|
2628
|
+
error("Missing value for --shell.");
|
|
2629
|
+
shell = normalizeEnvitShell(value);
|
|
2630
|
+
continue;
|
|
2631
|
+
}
|
|
2632
|
+
if (arg.startsWith("--shell=")) {
|
|
2633
|
+
shell = normalizeEnvitShell(arg.slice("--shell=".length));
|
|
2634
|
+
continue;
|
|
2635
|
+
}
|
|
2636
|
+
if (!configPath) {
|
|
2637
|
+
configPath = arg;
|
|
2638
|
+
continue;
|
|
2639
|
+
}
|
|
2640
|
+
error(`Unexpected argument '${arg}'. envit prints shell export commands and does not run a child process.`);
|
|
2641
|
+
}
|
|
2642
|
+
return { directory, configPath, shell, help };
|
|
2643
|
+
}
|
|
2644
|
+
function normalizeEnvitShell(value) {
|
|
2645
|
+
const normalized = value.trim().toLowerCase();
|
|
2646
|
+
if (normalized === "powershell" || normalized === "pwsh" || normalized === "ps1")
|
|
2647
|
+
return "powershell";
|
|
2648
|
+
if (normalized === "cmd" || normalized === "bat")
|
|
2649
|
+
return "cmd";
|
|
2650
|
+
if (normalized === "sh" || normalized === "bash" || normalized === "zsh")
|
|
2651
|
+
return "sh";
|
|
2652
|
+
if (normalized === "json")
|
|
2653
|
+
return "json";
|
|
2654
|
+
error(`Unsupported shell '${value}'. Use powershell, cmd, sh, or json.`);
|
|
2655
|
+
}
|
|
2656
|
+
function detectEnvitShell() {
|
|
2657
|
+
if (process.platform === "win32") {
|
|
2658
|
+
return "powershell";
|
|
2659
|
+
}
|
|
2660
|
+
return "sh";
|
|
2661
|
+
}
|
|
2662
|
+
function escapePowerShell(value) {
|
|
2663
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
2664
|
+
}
|
|
2665
|
+
function escapeCmd(value) {
|
|
2666
|
+
return value.replace(/"/g, '""');
|
|
2667
|
+
}
|
|
2668
|
+
function escapeSh(value) {
|
|
2669
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
2670
|
+
}
|
|
2671
|
+
function renderEnvitOutput(env, shell) {
|
|
2672
|
+
if (shell === "json") {
|
|
2673
|
+
return JSON.stringify(env, null, 2);
|
|
2674
|
+
}
|
|
2675
|
+
const lines = Object.entries(env).map(([key, value]) => {
|
|
2676
|
+
if (shell === "powershell") {
|
|
2677
|
+
return `$env:${key}=${escapePowerShell(value)}`;
|
|
2678
|
+
}
|
|
2679
|
+
if (shell === "cmd") {
|
|
2680
|
+
return `set "${key}=${escapeCmd(value)}"`;
|
|
2681
|
+
}
|
|
2682
|
+
return `export ${key}=${escapeSh(value)}`;
|
|
2683
|
+
});
|
|
2684
|
+
return lines.join(`
|
|
2685
|
+
`);
|
|
2686
|
+
}
|
|
2687
|
+
async function handleEnvit(options) {
|
|
2688
|
+
const cwd = options.directory || process.cwd();
|
|
2689
|
+
const configPath = options.configPath || ".config.toml";
|
|
2690
|
+
const shell = options.shell || detectEnvitShell();
|
|
2691
|
+
const { configEnv, exists } = await loadConfigEnv(cwd, configPath);
|
|
2692
|
+
if (!exists) {
|
|
2693
|
+
error(`Config file '${configPath}' not found.`);
|
|
2694
|
+
}
|
|
2695
|
+
console.log(renderEnvitOutput(configEnv, shell));
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
// src/commands/inline.ts
|
|
2699
|
+
init_utils();
|
|
2700
|
+
function parseInlineArgs(args) {
|
|
2701
|
+
let directory;
|
|
2702
|
+
let configPath;
|
|
2703
|
+
let help = false;
|
|
2704
|
+
const commandArgs = [];
|
|
2705
|
+
for (let i = 0;i < args.length; i++) {
|
|
2706
|
+
const arg = args[i];
|
|
2707
|
+
if (commandArgs.length > 0) {
|
|
2708
|
+
commandArgs.push(arg);
|
|
2709
|
+
continue;
|
|
2710
|
+
}
|
|
2711
|
+
if (arg === "--") {
|
|
2712
|
+
commandArgs.push(...args.slice(i + 1));
|
|
2713
|
+
break;
|
|
2714
|
+
}
|
|
2715
|
+
if (arg === "--help" || arg === "-h") {
|
|
2716
|
+
help = true;
|
|
2717
|
+
continue;
|
|
2718
|
+
}
|
|
2719
|
+
if (arg === "--directory") {
|
|
2720
|
+
directory = args[++i];
|
|
2721
|
+
if (!directory)
|
|
2722
|
+
error("Missing value for --directory.");
|
|
2723
|
+
continue;
|
|
2724
|
+
}
|
|
2725
|
+
if (arg.startsWith("--directory=")) {
|
|
2726
|
+
directory = arg.slice("--directory=".length);
|
|
2727
|
+
if (!directory)
|
|
2728
|
+
error("Missing value for --directory.");
|
|
2729
|
+
continue;
|
|
2730
|
+
}
|
|
2731
|
+
if (arg === "--config") {
|
|
2732
|
+
configPath = args[++i];
|
|
2733
|
+
if (!configPath)
|
|
2734
|
+
error("Missing value for --config.");
|
|
2735
|
+
continue;
|
|
2736
|
+
}
|
|
2737
|
+
if (arg.startsWith("--config=")) {
|
|
2738
|
+
configPath = arg.slice("--config=".length);
|
|
2739
|
+
if (!configPath)
|
|
2740
|
+
error("Missing value for --config.");
|
|
2741
|
+
continue;
|
|
2742
|
+
}
|
|
2743
|
+
commandArgs.push(...args.slice(i));
|
|
2744
|
+
break;
|
|
2745
|
+
}
|
|
2746
|
+
return { directory, configPath, commandArgs, help };
|
|
2747
|
+
}
|
|
2748
|
+
async function handleInline(options) {
|
|
2749
|
+
const cwd = options.directory || process.cwd();
|
|
2750
|
+
const configPath = options.configPath || ".config.toml";
|
|
2751
|
+
const { configEnv, exists } = await loadConfigEnv(cwd, configPath);
|
|
2752
|
+
if (exists) {
|
|
2753
|
+
console.log(`Loaded config from ${configPath}`);
|
|
2754
|
+
} else {
|
|
2755
|
+
console.log(`Config file '${configPath}' not found, continuing without it.`);
|
|
2756
|
+
}
|
|
2757
|
+
if (options.commandArgs.length === 0) {
|
|
2758
|
+
error("Please provide a command to run. Example: bgrun inline -- bun run dev");
|
|
2759
|
+
}
|
|
2760
|
+
const proc = Bun.spawn(options.commandArgs, {
|
|
2761
|
+
cwd,
|
|
2762
|
+
env: buildManagedProcessEnv(Bun.env, configEnv),
|
|
2763
|
+
stdin: "inherit",
|
|
2764
|
+
stdout: "inherit",
|
|
2765
|
+
stderr: "inherit"
|
|
2766
|
+
});
|
|
2767
|
+
const exitCode = await proc.exited;
|
|
2768
|
+
process.exit(exitCode);
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
// src/commands/guard.ts
|
|
2772
|
+
init_db();
|
|
2773
|
+
init_utils();
|
|
2774
|
+
async function handleGuardToggle(targetName, enabled) {
|
|
2775
|
+
if (!targetName) {
|
|
2776
|
+
error(`Please provide a process name. Example: bunx bgrun myapp ${enabled ? "--guard" : "--guard-off"}`);
|
|
2777
|
+
}
|
|
2778
|
+
if (isInternalProcessName(targetName)) {
|
|
2779
|
+
error(`'${targetName}' is an internal bgrun process.`);
|
|
2780
|
+
}
|
|
2781
|
+
const proc = getProcess(targetName);
|
|
2782
|
+
if (!proc) {
|
|
2783
|
+
error(`Process '${targetName}' not found.`);
|
|
2784
|
+
}
|
|
2785
|
+
const env = parseEnvString(proc.env || "");
|
|
2786
|
+
const alreadyGuarded = env.BGR_KEEP_ALIVE === "true";
|
|
2787
|
+
if (enabled) {
|
|
2788
|
+
env.BGR_KEEP_ALIVE = "true";
|
|
2789
|
+
await retryDatabaseOperation(() => updateProcessEnv(targetName, stringifyEnvString(env)));
|
|
2790
|
+
await ensureProcessWatcher(targetName);
|
|
2791
|
+
addHistoryEntry(targetName, "guard_on", proc.pid);
|
|
2792
|
+
announce(alreadyGuarded ? `Guard is already active for '${targetName}'` : `\uD83D\uDEE1\uFE0F Guard enabled for '${targetName}'`, "Process Guard");
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
delete env.BGR_KEEP_ALIVE;
|
|
2796
|
+
await retryDatabaseOperation(() => updateProcessEnv(targetName, stringifyEnvString(env)));
|
|
2797
|
+
await stopProcessWatcher(targetName);
|
|
2798
|
+
addHistoryEntry(targetName, "guard_off", proc.pid);
|
|
2799
|
+
announce(alreadyGuarded ? `\uD83D\uDED1 Guard disabled for '${targetName}'` : `Guard is already off for '${targetName}'`, "Process Guard");
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2305
2802
|
// src/index.ts
|
|
2306
|
-
init_logger();
|
|
2307
2803
|
init_platform();
|
|
2308
2804
|
init_db();
|
|
2309
2805
|
import dedent from "dedent";
|
|
2310
2806
|
import chalk7 from "chalk";
|
|
2311
|
-
import { join as
|
|
2807
|
+
import { join as join6 } from "path";
|
|
2312
2808
|
var {sleep: sleep3 } = globalThis.Bun;
|
|
2313
2809
|
import { configure } from "measure-fn";
|
|
2314
2810
|
if (!Bun.argv.includes("--_serve")) {
|
|
@@ -2356,32 +2852,50 @@ function redirectConsoleToFiles() {
|
|
|
2356
2852
|
};
|
|
2357
2853
|
}
|
|
2358
2854
|
}
|
|
2855
|
+
async function findDetachedProcessByArg(snippet) {
|
|
2856
|
+
if (process.platform !== "win32")
|
|
2857
|
+
return null;
|
|
2858
|
+
try {
|
|
2859
|
+
const result = await psExec(`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | Where-Object { $_.CommandLine -match '${snippet.replace(/'/g, "''")}' } | Sort-Object -Property CreationDate -Descending | Select-Object -First 1 -ExpandProperty ProcessId`, 3000);
|
|
2860
|
+
const pid = parseInt(result.trim(), 10);
|
|
2861
|
+
return !isNaN(pid) && pid > 0 ? pid : null;
|
|
2862
|
+
} catch {
|
|
2863
|
+
return null;
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2359
2866
|
async function showHelp() {
|
|
2360
2867
|
const usage = dedent`
|
|
2361
2868
|
${chalk7.bold("bgrun \u2014 Bun Background Runner")}
|
|
2362
2869
|
${chalk7.gray("\u2550".repeat(50))}
|
|
2363
2870
|
|
|
2364
2871
|
${chalk7.yellow("Usage:")}
|
|
2365
|
-
bgrun [name] [options]
|
|
2872
|
+
bunx bgrun [name] [options]
|
|
2366
2873
|
|
|
2367
2874
|
${chalk7.yellow("Commands:")}
|
|
2368
|
-
bgrun List all processes
|
|
2369
|
-
bgrun [name] Show details for a process
|
|
2370
|
-
bgrun --
|
|
2371
|
-
bgrun --
|
|
2372
|
-
bgrun --
|
|
2373
|
-
bgrun --
|
|
2374
|
-
bgrun
|
|
2375
|
-
bgrun --
|
|
2376
|
-
bgrun --
|
|
2377
|
-
bgrun --
|
|
2378
|
-
bgrun --
|
|
2875
|
+
bunx bgrun List all processes
|
|
2876
|
+
bunx bgrun [name] Show details for a process
|
|
2877
|
+
bunx bgrun -- <cmd> Start a managed process with an auto-generated name
|
|
2878
|
+
bunx bgrun inline -- <cmd> Run a command in this terminal with config env loaded
|
|
2879
|
+
bunx bgrun --env Print shell commands to export config env vars
|
|
2880
|
+
bunx bgrun --dashboard Launch web dashboard (managed by bgrun)
|
|
2881
|
+
bunx bgrun [name] --guard Enable crash watcher for a process
|
|
2882
|
+
bunx bgrun [name] --guard-off Disable crash watcher for a process
|
|
2883
|
+
bunx bgrun --restart [name] Restart a process
|
|
2884
|
+
bunx bgrun --restart-all Restart ALL registered processes
|
|
2885
|
+
bunx bgrun --stop [name] Stop a process (keep in registry)
|
|
2886
|
+
bunx bgrun --stop-all Stop ALL running processes
|
|
2887
|
+
bunx bgrun --delete [name] Delete a process
|
|
2888
|
+
bunx bgrun --clean Remove all stopped processes
|
|
2889
|
+
bunx bgrun --nuke Delete ALL processes
|
|
2379
2890
|
|
|
2380
2891
|
${chalk7.yellow("Options:")}
|
|
2381
2892
|
--name <string> Process name (required for new)
|
|
2382
2893
|
--command <string> Process command (required for new)
|
|
2383
2894
|
--directory <path> Working directory (required for new)
|
|
2384
2895
|
--config <path> Config file (default: .config.toml)
|
|
2896
|
+
--no-config Disable automatic .config.toml loading
|
|
2897
|
+
--env Print shell export commands from config and exit
|
|
2898
|
+
--shell <type> Shell for --env: powershell | cmd | sh | json
|
|
2385
2899
|
--watch Watch for file changes and auto-restart
|
|
2386
2900
|
--force Force restart existing process
|
|
2387
2901
|
--fetch Fetch latest git changes before running
|
|
@@ -2394,85 +2908,189 @@ async function showHelp() {
|
|
|
2394
2908
|
--version Show version
|
|
2395
2909
|
--debug Show debug info (DB path, BGR home, etc.)
|
|
2396
2910
|
--dashboard Launch web dashboard as bgrun-managed process
|
|
2911
|
+
--guard Enable per-process crash watcher
|
|
2912
|
+
--guard-off Disable per-process crash watcher
|
|
2397
2913
|
--port <number> Port for dashboard (default: 3000)
|
|
2398
2914
|
--help Show this help message
|
|
2399
2915
|
|
|
2400
2916
|
${chalk7.yellow("Examples:")}
|
|
2401
|
-
bgrun --
|
|
2402
|
-
bgrun --
|
|
2403
|
-
bgrun
|
|
2917
|
+
bunx bgrun -- bun run dev
|
|
2918
|
+
bunx bgrun --no-config -- bun run script.ts
|
|
2919
|
+
bunx bgrun --force -- bun run server.ts
|
|
2920
|
+
bunx bgrun inline -- bun run dev
|
|
2921
|
+
Invoke-Expression (bunx bgrun --env)
|
|
2922
|
+
eval "$(bunx bgrun --env --shell sh)"
|
|
2923
|
+
bunx bgrun --dashboard
|
|
2924
|
+
bunx bgrun myapp --guard
|
|
2925
|
+
bunx bgrun myapp --guard-off
|
|
2926
|
+
bunx bgrun --name myapp --command "bun run dev" --directory . --watch
|
|
2927
|
+
bunx bgrun myapp --logs --lines 50
|
|
2404
2928
|
`;
|
|
2405
2929
|
console.log(usage);
|
|
2406
2930
|
}
|
|
2931
|
+
var cliArgOptions = {
|
|
2932
|
+
name: { type: "string" },
|
|
2933
|
+
command: { type: "string" },
|
|
2934
|
+
directory: { type: "string" },
|
|
2935
|
+
config: { type: "string" },
|
|
2936
|
+
"no-config": { type: "boolean" },
|
|
2937
|
+
env: { type: "boolean" },
|
|
2938
|
+
shell: { type: "string" },
|
|
2939
|
+
watch: { type: "boolean" },
|
|
2940
|
+
force: { type: "boolean" },
|
|
2941
|
+
fetch: { type: "boolean" },
|
|
2942
|
+
delete: { type: "boolean" },
|
|
2943
|
+
nuke: { type: "boolean" },
|
|
2944
|
+
restart: { type: "boolean" },
|
|
2945
|
+
"restart-all": { type: "boolean" },
|
|
2946
|
+
stop: { type: "boolean" },
|
|
2947
|
+
"stop-all": { type: "boolean" },
|
|
2948
|
+
clean: { type: "boolean" },
|
|
2949
|
+
json: { type: "boolean" },
|
|
2950
|
+
logs: { type: "boolean" },
|
|
2951
|
+
"log-stdout": { type: "boolean" },
|
|
2952
|
+
"log-stderr": { type: "boolean" },
|
|
2953
|
+
lines: { type: "string" },
|
|
2954
|
+
filter: { type: "string" },
|
|
2955
|
+
version: { type: "boolean" },
|
|
2956
|
+
help: { type: "boolean" },
|
|
2957
|
+
db: { type: "string" },
|
|
2958
|
+
stdout: { type: "string" },
|
|
2959
|
+
stderr: { type: "string" },
|
|
2960
|
+
dashboard: { type: "boolean" },
|
|
2961
|
+
guard: { type: "boolean" },
|
|
2962
|
+
"guard-off": { type: "boolean" },
|
|
2963
|
+
debug: { type: "boolean" },
|
|
2964
|
+
_serve: { type: "boolean" },
|
|
2965
|
+
"_watch-process": { type: "string" },
|
|
2966
|
+
port: { type: "string" }
|
|
2967
|
+
};
|
|
2407
2968
|
async function run2() {
|
|
2969
|
+
const rawArgs = Bun.argv.slice(2);
|
|
2970
|
+
const isActionInvocation = (values2) => {
|
|
2971
|
+
return Boolean(values2.dashboard || values2.guard || values2["guard-off"] || values2.version || values2.help || values2.debug || values2.nuke || values2.clean || values2["restart-all"] || values2["stop-all"] || values2.delete || values2.restart || values2.stop || values2.logs || values2["log-stdout"] || values2["log-stderr"] || values2.watch || values2.json || values2.filter);
|
|
2972
|
+
};
|
|
2973
|
+
if (rawArgs[0] === "inline") {
|
|
2974
|
+
const parsed = parseInlineArgs(rawArgs.slice(1));
|
|
2975
|
+
if (parsed.help) {
|
|
2976
|
+
console.log(dedent`
|
|
2977
|
+
${chalk7.bold("bgrun inline")}
|
|
2978
|
+
${chalk7.gray("\u2500".repeat(40))}
|
|
2979
|
+
|
|
2980
|
+
Run a command in the current terminal with env vars loaded from a bgrun config file.
|
|
2981
|
+
|
|
2982
|
+
Usage:
|
|
2983
|
+
bunx bgrun inline [--directory <path>] [--config <path>] -- <command> [args...]
|
|
2984
|
+
|
|
2985
|
+
Examples:
|
|
2986
|
+
bunx bgrun inline -- bun run dev
|
|
2987
|
+
bunx bgrun inline --directory apps/api -- node server.js
|
|
2988
|
+
`);
|
|
2989
|
+
return;
|
|
2990
|
+
}
|
|
2991
|
+
await handleInline(parsed);
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
const delimiterIndex = rawArgs.indexOf("--");
|
|
2995
|
+
if (delimiterIndex !== -1 && delimiterIndex < rawArgs.length - 1) {
|
|
2996
|
+
const preArgs = rawArgs.slice(0, delimiterIndex);
|
|
2997
|
+
const commandArgs = rawArgs.slice(delimiterIndex + 1);
|
|
2998
|
+
const { values: values2 } = parseArgs({
|
|
2999
|
+
args: preArgs,
|
|
3000
|
+
options: cliArgOptions,
|
|
3001
|
+
strict: false,
|
|
3002
|
+
allowPositionals: true
|
|
3003
|
+
});
|
|
3004
|
+
const autoName = values2.name || generateAutoProcessName();
|
|
3005
|
+
const inlineCommand = joinCommandArgs(commandArgs);
|
|
3006
|
+
const directory = values2.directory || process.cwd();
|
|
3007
|
+
await handleRun({
|
|
3008
|
+
action: "run",
|
|
3009
|
+
name: autoName,
|
|
3010
|
+
command: inlineCommand,
|
|
3011
|
+
directory,
|
|
3012
|
+
configPath: values2["no-config"] ? "" : values2.config,
|
|
3013
|
+
force: values2.force,
|
|
3014
|
+
fetch: values2.fetch,
|
|
3015
|
+
remoteName: "",
|
|
3016
|
+
dbPath: values2.db,
|
|
3017
|
+
stdout: values2.stdout,
|
|
3018
|
+
stderr: values2.stderr
|
|
3019
|
+
});
|
|
3020
|
+
return;
|
|
3021
|
+
}
|
|
2408
3022
|
const { values, positionals } = parseArgs({
|
|
2409
|
-
args:
|
|
2410
|
-
options:
|
|
2411
|
-
name: { type: "string" },
|
|
2412
|
-
command: { type: "string" },
|
|
2413
|
-
directory: { type: "string" },
|
|
2414
|
-
config: { type: "string" },
|
|
2415
|
-
watch: { type: "boolean" },
|
|
2416
|
-
force: { type: "boolean" },
|
|
2417
|
-
fetch: { type: "boolean" },
|
|
2418
|
-
delete: { type: "boolean" },
|
|
2419
|
-
nuke: { type: "boolean" },
|
|
2420
|
-
restart: { type: "boolean" },
|
|
2421
|
-
"restart-all": { type: "boolean" },
|
|
2422
|
-
stop: { type: "boolean" },
|
|
2423
|
-
"stop-all": { type: "boolean" },
|
|
2424
|
-
clean: { type: "boolean" },
|
|
2425
|
-
json: { type: "boolean" },
|
|
2426
|
-
logs: { type: "boolean" },
|
|
2427
|
-
"log-stdout": { type: "boolean" },
|
|
2428
|
-
"log-stderr": { type: "boolean" },
|
|
2429
|
-
lines: { type: "string" },
|
|
2430
|
-
filter: { type: "string" },
|
|
2431
|
-
version: { type: "boolean" },
|
|
2432
|
-
help: { type: "boolean" },
|
|
2433
|
-
db: { type: "string" },
|
|
2434
|
-
stdout: { type: "string" },
|
|
2435
|
-
stderr: { type: "string" },
|
|
2436
|
-
dashboard: { type: "boolean" },
|
|
2437
|
-
guard: { type: "boolean" },
|
|
2438
|
-
debug: { type: "boolean" },
|
|
2439
|
-
_serve: { type: "boolean" },
|
|
2440
|
-
"_guard-loop": { type: "boolean" },
|
|
2441
|
-
port: { type: "string" }
|
|
2442
|
-
},
|
|
3023
|
+
args: rawArgs,
|
|
3024
|
+
options: cliArgOptions,
|
|
2443
3025
|
strict: false,
|
|
2444
3026
|
allowPositionals: true
|
|
2445
3027
|
});
|
|
3028
|
+
if (values.env) {
|
|
3029
|
+
if (positionals.length > 1) {
|
|
3030
|
+
error("Too many positional arguments for --env. Use --config <path> or pass a single config path.");
|
|
3031
|
+
}
|
|
3032
|
+
await handleEnvit({
|
|
3033
|
+
directory: values.directory,
|
|
3034
|
+
configPath: values.config || positionals[0],
|
|
3035
|
+
shell: values.shell
|
|
3036
|
+
});
|
|
3037
|
+
return;
|
|
3038
|
+
}
|
|
3039
|
+
if (positionals.length > 1 && !values.command && !isActionInvocation(values)) {
|
|
3040
|
+
const autoName = values.name || generateAutoProcessName();
|
|
3041
|
+
const implicitCommand = joinCommandArgs(positionals);
|
|
3042
|
+
const directory = values.directory || process.cwd();
|
|
3043
|
+
await handleRun({
|
|
3044
|
+
action: "run",
|
|
3045
|
+
name: autoName,
|
|
3046
|
+
command: implicitCommand,
|
|
3047
|
+
directory,
|
|
3048
|
+
configPath: values["no-config"] ? "" : values.config,
|
|
3049
|
+
force: values.force,
|
|
3050
|
+
fetch: values.fetch,
|
|
3051
|
+
remoteName: "",
|
|
3052
|
+
dbPath: values.db,
|
|
3053
|
+
stdout: values.stdout,
|
|
3054
|
+
stderr: values.stderr
|
|
3055
|
+
});
|
|
3056
|
+
return;
|
|
3057
|
+
}
|
|
2446
3058
|
if (values["_serve"]) {
|
|
2447
3059
|
redirectConsoleToFiles();
|
|
2448
|
-
const { startServer: startServer2 } = await
|
|
3060
|
+
const { startServer: startServer2 } = await init_server().then(() => exports_server);
|
|
2449
3061
|
await startServer2();
|
|
2450
3062
|
return;
|
|
2451
3063
|
}
|
|
2452
|
-
if (values["
|
|
3064
|
+
if (values["_watch-process"]) {
|
|
2453
3065
|
redirectConsoleToFiles();
|
|
2454
|
-
|
|
2455
|
-
const intervalStr = positionals[0];
|
|
2456
|
-
const intervalMs = intervalStr ? parseInt(intervalStr) * 1000 : undefined;
|
|
2457
|
-
await startGuardLoop2(intervalMs);
|
|
3066
|
+
await startProcessWatcher(String(values["_watch-process"]));
|
|
2458
3067
|
return;
|
|
2459
3068
|
}
|
|
2460
3069
|
if (values.dashboard) {
|
|
2461
3070
|
const dashboardName = "bgr-dashboard";
|
|
2462
3071
|
const homePath3 = getHomeDir();
|
|
2463
|
-
const bgrDir2 =
|
|
3072
|
+
const bgrDir2 = join6(homePath3, ".bgr");
|
|
2464
3073
|
const requestedPort = values.port;
|
|
3074
|
+
const explicitPortValue = requestedPort || Bun.env.BUN_PORT || undefined;
|
|
3075
|
+
const explicitPort = explicitPortValue ? parseInt(explicitPortValue, 10) : null;
|
|
2465
3076
|
const existing = getProcess(dashboardName);
|
|
2466
3077
|
if (existing && await isProcessRunning(existing.pid)) {
|
|
2467
|
-
|
|
3078
|
+
const resolved = await resolvePidWithPorts(existing.pid);
|
|
3079
|
+
let existingPid = resolved.pid;
|
|
3080
|
+
let existingPorts = resolved.ports;
|
|
2468
3081
|
if (existingPorts.length === 0) {
|
|
2469
|
-
const
|
|
2470
|
-
if (
|
|
2471
|
-
|
|
3082
|
+
const detachedPid = await findDetachedProcessByArg("--_serve");
|
|
3083
|
+
if (detachedPid && detachedPid !== existingPid) {
|
|
3084
|
+
const detachedResolved = await resolvePidWithPorts(detachedPid);
|
|
3085
|
+
existingPid = detachedResolved.pid;
|
|
3086
|
+
existingPorts = detachedResolved.ports;
|
|
2472
3087
|
}
|
|
2473
3088
|
}
|
|
3089
|
+
if (existingPid !== existing.pid && existingPorts.length > 0) {
|
|
3090
|
+
await retryDatabaseOperation(() => updateProcessPid(dashboardName, existingPid));
|
|
3091
|
+
}
|
|
2474
3092
|
const portStr = existingPorts.length > 0 ? `:${existingPorts[0]}` : "(detecting...)";
|
|
2475
|
-
announce(`Dashboard is already running (PID ${
|
|
3093
|
+
announce(`Dashboard is already running (PID ${existingPid})
|
|
2476
3094
|
|
|
2477
3095
|
` + ` \uD83C\uDF10 ${chalk7.cyan(`http://localhost${portStr}`)}
|
|
2478
3096
|
|
|
@@ -2491,29 +3109,28 @@ async function run2() {
|
|
|
2491
3109
|
}
|
|
2492
3110
|
await retryDatabaseOperation(() => removeProcessByName(dashboardName));
|
|
2493
3111
|
}
|
|
2494
|
-
const
|
|
2495
|
-
const
|
|
2496
|
-
const
|
|
2497
|
-
const
|
|
2498
|
-
const stdoutPath = join4(bgrDir2, `${dashboardName}-out.txt`);
|
|
2499
|
-
const stderrPath = join4(bgrDir2, `${dashboardName}-err.txt`);
|
|
3112
|
+
const spawnCommand = `bunx bgrun --_serve`;
|
|
3113
|
+
const command = `bunx bgrun --_serve`;
|
|
3114
|
+
const stdoutPath = join6(bgrDir2, `${dashboardName}-out.txt`);
|
|
3115
|
+
const stderrPath = join6(bgrDir2, `${dashboardName}-err.txt`);
|
|
2500
3116
|
await Bun.write(stdoutPath, "");
|
|
2501
3117
|
await Bun.write(stderrPath, "");
|
|
2502
3118
|
const spawnEnv = { ...Bun.env };
|
|
2503
|
-
if (
|
|
2504
|
-
spawnEnv.BUN_PORT =
|
|
3119
|
+
if (explicitPortValue) {
|
|
3120
|
+
spawnEnv.BUN_PORT = explicitPortValue;
|
|
3121
|
+
} else {
|
|
3122
|
+
delete spawnEnv.BUN_PORT;
|
|
2505
3123
|
}
|
|
2506
3124
|
spawnEnv.BGR_STDOUT = stdoutPath;
|
|
2507
3125
|
spawnEnv.BGR_STDERR = stderrPath;
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
const portFree = await isPortFree(targetPort);
|
|
3126
|
+
if (explicitPort && explicitPort > 0) {
|
|
3127
|
+
const portFree = await isPortFree(explicitPort);
|
|
2511
3128
|
if (!portFree) {
|
|
2512
|
-
console.log(chalk7.yellow(` \u26A1
|
|
2513
|
-
await killProcessOnPort(
|
|
2514
|
-
const freed = await waitForPortFree(
|
|
3129
|
+
console.log(chalk7.yellow(` \u26A1 Requested dashboard port ${explicitPort} is occupied \u2014 reclaiming...`));
|
|
3130
|
+
await killProcessOnPort(explicitPort);
|
|
3131
|
+
const freed = await waitForPortFree(explicitPort, 5000);
|
|
2515
3132
|
if (!freed) {
|
|
2516
|
-
console.log(chalk7.red(` \u26A0 Could not free port ${
|
|
3133
|
+
console.log(chalk7.red(` \u26A0 Could not free port ${explicitPort} \u2014 dashboard may pick a fallback port`));
|
|
2517
3134
|
}
|
|
2518
3135
|
}
|
|
2519
3136
|
}
|
|
@@ -2526,15 +3143,24 @@ async function run2() {
|
|
|
2526
3143
|
});
|
|
2527
3144
|
newProcess.unref();
|
|
2528
3145
|
await sleep3(2000);
|
|
2529
|
-
|
|
2530
|
-
|
|
3146
|
+
let actualPid = explicitPort && explicitPort > 0 ? await findPidByPort(explicitPort, 1e4) ?? await findChildPid(newProcess.pid) : await findChildPid(newProcess.pid);
|
|
3147
|
+
if (!await isProcessRunning(actualPid)) {
|
|
3148
|
+
const detachedPid = await findDetachedProcessByArg("--_serve");
|
|
3149
|
+
if (detachedPid)
|
|
3150
|
+
actualPid = detachedPid;
|
|
3151
|
+
}
|
|
2531
3152
|
let actualPort = null;
|
|
2532
3153
|
for (let attempt = 0;attempt < 10; attempt++) {
|
|
2533
|
-
const
|
|
2534
|
-
|
|
2535
|
-
|
|
3154
|
+
const resolved = await resolvePidWithPorts(actualPid);
|
|
3155
|
+
actualPid = resolved.pid;
|
|
3156
|
+
if (resolved.ports.length > 0) {
|
|
3157
|
+
actualPort = resolved.ports[0];
|
|
2536
3158
|
break;
|
|
2537
3159
|
}
|
|
3160
|
+
const detachedPid = await findDetachedProcessByArg("--_serve");
|
|
3161
|
+
if (detachedPid && detachedPid !== actualPid) {
|
|
3162
|
+
actualPid = detachedPid;
|
|
3163
|
+
}
|
|
2538
3164
|
await sleep3(1000);
|
|
2539
3165
|
}
|
|
2540
3166
|
await retryDatabaseOperation(() => insertProcess({
|
|
@@ -2567,75 +3193,8 @@ async function run2() {
|
|
|
2567
3193
|
announce(msg, "BGR Dashboard");
|
|
2568
3194
|
return;
|
|
2569
3195
|
}
|
|
2570
|
-
if (values.guard) {
|
|
2571
|
-
|
|
2572
|
-
const homePath3 = getHomeDir();
|
|
2573
|
-
const bgrDir2 = join4(homePath3, ".bgr");
|
|
2574
|
-
const existing = getProcess(guardName);
|
|
2575
|
-
if (existing && await isProcessRunning(existing.pid)) {
|
|
2576
|
-
announce(`Guard is already running (PID ${existing.pid})
|
|
2577
|
-
|
|
2578
|
-
Use ${chalk7.yellow(`bgrun --stop ${guardName}`)} to stop it
|
|
2579
|
-
Use ${chalk7.yellow(`bgrun --guard --force`)} to restart`, "BGR Guard");
|
|
2580
|
-
return;
|
|
2581
|
-
}
|
|
2582
|
-
if (existing) {
|
|
2583
|
-
if (await isProcessRunning(existing.pid)) {
|
|
2584
|
-
await terminateProcess(existing.pid);
|
|
2585
|
-
}
|
|
2586
|
-
await retryDatabaseOperation(() => removeProcessByName(guardName));
|
|
2587
|
-
}
|
|
2588
|
-
const { resolve } = __require("path");
|
|
2589
|
-
const scriptPath = resolve(process.argv[1]);
|
|
2590
|
-
const spawnCommand = `bun run ${scriptPath} --_guard-loop`;
|
|
2591
|
-
const command = `bgrun --_guard-loop`;
|
|
2592
|
-
const stdoutPath = join4(bgrDir2, `${guardName}-out.txt`);
|
|
2593
|
-
const stderrPath = join4(bgrDir2, `${guardName}-err.txt`);
|
|
2594
|
-
await Bun.write(stdoutPath, "");
|
|
2595
|
-
await Bun.write(stderrPath, "");
|
|
2596
|
-
const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
|
|
2597
|
-
env: { ...Bun.env, BGR_STDOUT: stdoutPath, BGR_STDERR: stderrPath },
|
|
2598
|
-
cwd: bgrDir2,
|
|
2599
|
-
stdout: "ignore",
|
|
2600
|
-
stderr: "ignore",
|
|
2601
|
-
detached: true
|
|
2602
|
-
});
|
|
2603
|
-
newProcess.unref();
|
|
2604
|
-
await sleep3(1000);
|
|
2605
|
-
let actualPid = await findChildPid(newProcess.pid);
|
|
2606
|
-
if (!await isProcessRunning(actualPid)) {
|
|
2607
|
-
const { psExec: ps } = await Promise.resolve().then(() => (init_platform(), exports_platform));
|
|
2608
|
-
const result = ps(`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | Where-Object { $_.CommandLine -match '_guard-loop' } | Sort-Object -Property CreationDate -Descending | Select-Object -First 1 -ExpandProperty ProcessId`, 3000);
|
|
2609
|
-
const foundPid = parseInt(result.trim());
|
|
2610
|
-
if (!isNaN(foundPid) && foundPid > 0)
|
|
2611
|
-
actualPid = foundPid;
|
|
2612
|
-
}
|
|
2613
|
-
await retryDatabaseOperation(() => insertProcess({
|
|
2614
|
-
pid: actualPid,
|
|
2615
|
-
workdir: bgrDir2,
|
|
2616
|
-
command,
|
|
2617
|
-
name: guardName,
|
|
2618
|
-
env: "BGR_KEEP_ALIVE=false",
|
|
2619
|
-
configPath: "",
|
|
2620
|
-
stdout_path: stdoutPath,
|
|
2621
|
-
stderr_path: stderrPath
|
|
2622
|
-
}));
|
|
2623
|
-
const msg = dedent`
|
|
2624
|
-
${chalk7.bold("\uD83D\uDEE1\uFE0F BGR Standalone Guard launched")}
|
|
2625
|
-
${chalk7.gray("\u2500".repeat(40))}
|
|
2626
|
-
|
|
2627
|
-
Monitors: All processes with BGR_KEEP_ALIVE=true
|
|
2628
|
-
Also watches: bgr-dashboard (auto-restart if it dies)
|
|
2629
|
-
Check interval: 30 seconds
|
|
2630
|
-
Backoff: Exponential after 5 rapid crashes
|
|
2631
|
-
|
|
2632
|
-
${chalk7.gray("\u2500".repeat(40))}
|
|
2633
|
-
Process: ${chalk7.white(guardName)} | PID: ${chalk7.white(String(actualPid))}
|
|
2634
|
-
|
|
2635
|
-
${chalk7.yellow(`bgrun ${guardName} --logs`)} View guard logs
|
|
2636
|
-
${chalk7.yellow(`bgrun --stop ${guardName}`)} Stop the guard
|
|
2637
|
-
`;
|
|
2638
|
-
announce(msg, "BGR Guard");
|
|
3196
|
+
if (values.guard || values["guard-off"]) {
|
|
3197
|
+
await handleGuardToggle(positionals[0], Boolean(values.guard));
|
|
2639
3198
|
return;
|
|
2640
3199
|
}
|
|
2641
3200
|
if (values.version) {
|
|
@@ -2797,7 +3356,7 @@ async function run2() {
|
|
|
2797
3356
|
name,
|
|
2798
3357
|
command: values.command,
|
|
2799
3358
|
directory: values.directory,
|
|
2800
|
-
configPath: values.config,
|
|
3359
|
+
configPath: values["no-config"] ? "" : values.config,
|
|
2801
3360
|
force: values.force,
|
|
2802
3361
|
fetch: values.fetch,
|
|
2803
3362
|
remoteName: "",
|