bgrun 3.12.15 → 3.12.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -14
- 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 +1275 -730
- package/dist/server.js +165 -664
- 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,6 +1149,102 @@ var init_log_rotation = __esm(() => {
|
|
|
1068
1149
|
DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
|
1069
1150
|
});
|
|
1070
1151
|
|
|
1152
|
+
// src/server.ts
|
|
1153
|
+
var exports_server = {};
|
|
1154
|
+
__export(exports_server, {
|
|
1155
|
+
startServer: () => startServer,
|
|
1156
|
+
guardRestartCounts: () => guardRestartCounts,
|
|
1157
|
+
guardEvents: () => guardEvents
|
|
1158
|
+
});
|
|
1159
|
+
import path from "path";
|
|
1160
|
+
async function cleanupPort(port) {
|
|
1161
|
+
if (process.platform !== "win32")
|
|
1162
|
+
return port;
|
|
1163
|
+
try {
|
|
1164
|
+
const occupiedBefore = !await isPortFree(port);
|
|
1165
|
+
if (!occupiedBefore)
|
|
1166
|
+
return port;
|
|
1167
|
+
console.log(`[server] Reclaiming port ${port} before dashboard start`);
|
|
1168
|
+
await killProcessOnPort(port);
|
|
1169
|
+
const freed = await waitForPortFree(port, 8000);
|
|
1170
|
+
if (freed)
|
|
1171
|
+
return port;
|
|
1172
|
+
console.warn(`[server] Port ${port} still busy after first cleanup; retrying`);
|
|
1173
|
+
await killProcessOnPort(port);
|
|
1174
|
+
const freedAfterRetry = await waitForPortFree(port, 5000);
|
|
1175
|
+
if (freedAfterRetry)
|
|
1176
|
+
return port;
|
|
1177
|
+
const fallback = port + 1;
|
|
1178
|
+
console.warn(`[server] \u26A0 Could not reclaim port ${port}; falling back to port ${fallback}`);
|
|
1179
|
+
return fallback;
|
|
1180
|
+
} catch {
|
|
1181
|
+
return port;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
async function startServer() {
|
|
1185
|
+
const { start } = await import("melina");
|
|
1186
|
+
const appDir = path.join(import.meta.dir, "../dashboard/app");
|
|
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;
|
|
1191
|
+
_originalPort = requestedPort;
|
|
1192
|
+
const resolvedPort = hasExplicitPort ? await cleanupPort(requestedPort) : requestedPort;
|
|
1193
|
+
_currentPort = resolvedPort;
|
|
1194
|
+
const needsExplicitPort = hasExplicitPort || resolvedPort !== requestedPort;
|
|
1195
|
+
await start({
|
|
1196
|
+
appDir,
|
|
1197
|
+
defaultTitle: "bgrun Dashboard - Process Manager",
|
|
1198
|
+
globalCss: path.join(appDir, "globals.css"),
|
|
1199
|
+
...needsExplicitPort && { port: resolvedPort }
|
|
1200
|
+
});
|
|
1201
|
+
const { startLogRotation: startLogRotation2 } = await Promise.resolve().then(() => (init_log_rotation(), exports_log_rotation));
|
|
1202
|
+
startLogRotation2(() => getAllProcesses());
|
|
1203
|
+
if (resolvedPort !== requestedPort) {
|
|
1204
|
+
startStickyPortChecker();
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
function startStickyPortChecker() {
|
|
1208
|
+
const CHECK_INTERVAL_MS = 60000;
|
|
1209
|
+
console.log(`[server] Starting sticky port checker (original: ${_originalPort}, current: ${_currentPort})`);
|
|
1210
|
+
setInterval(async () => {
|
|
1211
|
+
if (_currentPort === _originalPort)
|
|
1212
|
+
return;
|
|
1213
|
+
try {
|
|
1214
|
+
const proc = Bun.spawn([
|
|
1215
|
+
"powershell",
|
|
1216
|
+
"-NoProfile",
|
|
1217
|
+
"-Command",
|
|
1218
|
+
`Get-NetTCPConnection -LocalPort ${_originalPort} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
|
|
1219
|
+
], { stdout: "pipe", stderr: "pipe" });
|
|
1220
|
+
const text = await new Response(proc.stdout).text();
|
|
1221
|
+
const pid = parseInt(text.trim(), 10);
|
|
1222
|
+
if (!pid) {
|
|
1223
|
+
console.log(`[server] \u2713 Original port ${_originalPort} is now available! Consider restarting to reclaim it.`);
|
|
1224
|
+
_currentPort = _originalPort;
|
|
1225
|
+
}
|
|
1226
|
+
} catch {}
|
|
1227
|
+
}, CHECK_INTERVAL_MS);
|
|
1228
|
+
}
|
|
1229
|
+
var guardRestartCounts, guardEvents, _originalPort = 3000, _currentPort = 3000;
|
|
1230
|
+
var init_server = __esm(async () => {
|
|
1231
|
+
init_db();
|
|
1232
|
+
init_platform();
|
|
1233
|
+
guardRestartCounts = new Map;
|
|
1234
|
+
guardEvents = [];
|
|
1235
|
+
if (import.meta.main) {
|
|
1236
|
+
await startServer();
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
|
|
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
|
+
|
|
1071
1248
|
// src/logger.ts
|
|
1072
1249
|
import boxen from "boxen";
|
|
1073
1250
|
import chalk from "chalk";
|
|
@@ -1081,6 +1258,13 @@ function announce(message, title) {
|
|
|
1081
1258
|
borderStyle: "round"
|
|
1082
1259
|
}));
|
|
1083
1260
|
}
|
|
1261
|
+
|
|
1262
|
+
class BgrunError extends Error {
|
|
1263
|
+
constructor(message) {
|
|
1264
|
+
super(message);
|
|
1265
|
+
this.name = "BgrunError";
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1084
1268
|
function error(message) {
|
|
1085
1269
|
const text = message instanceof Error ? message.stack || message.message : String(message);
|
|
1086
1270
|
console.error(boxen(chalk.red(text), {
|
|
@@ -1093,15 +1277,9 @@ function error(message) {
|
|
|
1093
1277
|
}));
|
|
1094
1278
|
throw new BgrunError(text);
|
|
1095
1279
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
constructor(message) {
|
|
1100
|
-
super(message);
|
|
1101
|
-
this.name = "BgrunError";
|
|
1102
|
-
}
|
|
1103
|
-
};
|
|
1104
|
-
});
|
|
1280
|
+
|
|
1281
|
+
// src/commands/run.ts
|
|
1282
|
+
init_utils();
|
|
1105
1283
|
|
|
1106
1284
|
// src/config.ts
|
|
1107
1285
|
function formatEnvKey(key) {
|
|
@@ -1133,516 +1311,553 @@ async function parseConfigFile(configPath) {
|
|
|
1133
1311
|
const parsedConfig = await import(importPath).then((m) => m.default);
|
|
1134
1312
|
return flattenConfig(parsedConfig);
|
|
1135
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
|
+
}
|
|
1136
1330
|
|
|
1137
1331
|
// src/commands/run.ts
|
|
1138
1332
|
var {$: $2 } = globalThis.Bun;
|
|
1139
1333
|
var {sleep: sleep2 } = globalThis.Bun;
|
|
1140
|
-
import { join as
|
|
1334
|
+
import { join as join5 } from "path";
|
|
1141
1335
|
import { createMeasure as createMeasure2 } from "measure-fn";
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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;
|
|
1159
1501
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
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;
|
|
1548
|
+
try {
|
|
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;
|
|
1167
1560
|
}
|
|
1168
|
-
|
|
1561
|
+
if (proc.pid <= 0 || isProcessOperationLocked(targetName)) {
|
|
1562
|
+
await Bun.sleep(intervalMs);
|
|
1563
|
+
continue;
|
|
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
|
+
}
|
|
1169
1572
|
try {
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
}
|
|
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 });
|
|
1177
1585
|
} catch (err) {
|
|
1178
|
-
|
|
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}`);
|
|
1179
1588
|
}
|
|
1180
|
-
})
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
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);
|
|
1189
1600
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
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
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1195
1672
|
}
|
|
1196
|
-
if (
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
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.`);
|
|
1200
1680
|
}
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
await
|
|
1205
|
-
await
|
|
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");
|
|
1689
|
+
}
|
|
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
|
|
1206
1707
|
}
|
|
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);
|
|
1207
1714
|
}
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
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);
|
|
1218
1731
|
}
|
|
1219
|
-
const
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
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);
|
|
1737
|
+
}
|
|
1225
1738
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
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
|
+
}
|
|
1228
1755
|
}
|
|
1229
|
-
}
|
|
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}`);
|
|
1756
|
+
});
|
|
1268
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));
|
|
1269
1788
|
} else {
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
const
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
})
|
|
1284
|
-
|
|
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
|
-
// src/server.ts
|
|
1313
|
-
var exports_server = {};
|
|
1314
|
-
__export(exports_server, {
|
|
1315
|
-
startServer: () => startServer,
|
|
1316
|
-
guardRestartCounts: () => guardRestartCounts,
|
|
1317
|
-
guardEvents: () => guardEvents
|
|
1318
|
-
});
|
|
1319
|
-
import path from "path";
|
|
1320
|
-
async function cleanupPort(port) {
|
|
1321
|
-
if (process.platform !== "win32")
|
|
1322
|
-
return port;
|
|
1323
|
-
try {
|
|
1324
|
-
const proc = Bun.spawn([
|
|
1325
|
-
"powershell",
|
|
1326
|
-
"-NoProfile",
|
|
1327
|
-
"-Command",
|
|
1328
|
-
`Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
|
|
1329
|
-
], { stdout: "pipe", stderr: "pipe" });
|
|
1330
|
-
const text = await new Response(proc.stdout).text();
|
|
1331
|
-
const pid = parseInt(text.trim(), 10);
|
|
1332
|
-
if (!pid || pid === process.pid)
|
|
1333
|
-
return port;
|
|
1334
|
-
const checkProc = Bun.spawn([
|
|
1335
|
-
"powershell",
|
|
1336
|
-
"-NoProfile",
|
|
1337
|
-
"-Command",
|
|
1338
|
-
`Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`
|
|
1339
|
-
], { stdout: "pipe", stderr: "pipe" });
|
|
1340
|
-
const checkText = await new Response(checkProc.stdout).text();
|
|
1341
|
-
if (checkText.trim()) {
|
|
1342
|
-
console.log(`[server] Killing PID ${pid} holding port ${port}`);
|
|
1343
|
-
Bun.spawn(["taskkill", "/F", "/PID", String(pid)], { stdout: "pipe", stderr: "pipe" });
|
|
1344
|
-
await Bun.sleep(1000);
|
|
1345
|
-
return port;
|
|
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;
|
|
1346
1804
|
} else {
|
|
1347
|
-
|
|
1348
|
-
console.log(`[server] \u26A0 Port ${port} held by zombie PID ${pid} \u2014 falling back to port ${fallback}`);
|
|
1349
|
-
return fallback;
|
|
1805
|
+
finalConfigPath = ".config.toml";
|
|
1350
1806
|
}
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
}
|
|
1355
|
-
async function startServer() {
|
|
1356
|
-
const { start } = await import("melina");
|
|
1357
|
-
const appDir = path.join(import.meta.dir, "../dashboard/app");
|
|
1358
|
-
const requestedPort = process.env.BUN_PORT ? parseInt(process.env.BUN_PORT, 10) : 3000;
|
|
1359
|
-
_originalPort = requestedPort;
|
|
1360
|
-
const resolvedPort = await cleanupPort(requestedPort);
|
|
1361
|
-
_currentPort = resolvedPort;
|
|
1362
|
-
const needsExplicitPort = process.env.BUN_PORT || resolvedPort !== requestedPort;
|
|
1363
|
-
await start({
|
|
1364
|
-
appDir,
|
|
1365
|
-
defaultTitle: "bgrun Dashboard - Process Manager",
|
|
1366
|
-
globalCss: path.join(appDir, "globals.css"),
|
|
1367
|
-
...needsExplicitPort && { port: resolvedPort }
|
|
1368
|
-
});
|
|
1369
|
-
startGuard();
|
|
1370
|
-
const { startLogRotation: startLogRotation2 } = await Promise.resolve().then(() => (init_log_rotation(), exports_log_rotation));
|
|
1371
|
-
startLogRotation2(() => getAllProcesses());
|
|
1372
|
-
if (resolvedPort !== requestedPort) {
|
|
1373
|
-
startStickyPortChecker();
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
function startStickyPortChecker() {
|
|
1377
|
-
const CHECK_INTERVAL_MS = 60000;
|
|
1378
|
-
console.log(`[server] Starting sticky port checker (original: ${_originalPort}, current: ${_currentPort})`);
|
|
1379
|
-
setInterval(async () => {
|
|
1380
|
-
if (_currentPort === _originalPort)
|
|
1381
|
-
return;
|
|
1382
|
-
try {
|
|
1383
|
-
const proc = Bun.spawn([
|
|
1384
|
-
"powershell",
|
|
1385
|
-
"-NoProfile",
|
|
1386
|
-
"-Command",
|
|
1387
|
-
`Get-NetTCPConnection -LocalPort ${_originalPort} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
|
|
1388
|
-
], { stdout: "pipe", stderr: "pipe" });
|
|
1389
|
-
const text = await new Response(proc.stdout).text();
|
|
1390
|
-
const pid = parseInt(text.trim(), 10);
|
|
1391
|
-
if (!pid) {
|
|
1392
|
-
console.log(`[server] \u2713 Original port ${_originalPort} is now available! Consider restarting to reclaim it.`);
|
|
1393
|
-
_currentPort = _originalPort;
|
|
1394
|
-
}
|
|
1395
|
-
} catch {}
|
|
1396
|
-
}, CHECK_INTERVAL_MS);
|
|
1397
|
-
}
|
|
1398
|
-
function startGuard() {
|
|
1399
|
-
console.log(`[guard] \u2713 Built-in process guard started (checking every ${GUARD_INTERVAL_MS / 1000}s)`);
|
|
1400
|
-
setInterval(async () => {
|
|
1401
|
-
try {
|
|
1402
|
-
const processes = getAllProcesses();
|
|
1403
|
-
if (processes.length === 0)
|
|
1404
|
-
return;
|
|
1405
|
-
for (const proc of processes) {
|
|
1406
|
-
if (GUARD_SKIP_NAMES.has(proc.name))
|
|
1407
|
-
continue;
|
|
1408
|
-
const env = proc.env ? parseEnvString(proc.env) : {};
|
|
1409
|
-
if (env.BGR_KEEP_ALIVE !== "true")
|
|
1410
|
-
continue;
|
|
1411
|
-
const alive = await isProcessRunning(proc.pid, proc.command);
|
|
1412
|
-
if (!alive) {
|
|
1413
|
-
const now = Date.now();
|
|
1414
|
-
const nextRestart = guardNextRestartTime.get(proc.name) || 0;
|
|
1415
|
-
if (now < nextRestart)
|
|
1416
|
-
continue;
|
|
1417
|
-
console.log(`[guard] \u26A0 Guarded process "${proc.name}" (PID ${proc.pid}) is dead, restarting...`);
|
|
1418
|
-
let success = false;
|
|
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 () => {
|
|
1419
1811
|
try {
|
|
1420
|
-
await
|
|
1421
|
-
action: "run",
|
|
1422
|
-
name: proc.name,
|
|
1423
|
-
force: true,
|
|
1424
|
-
remoteName: ""
|
|
1425
|
-
});
|
|
1426
|
-
success = true;
|
|
1427
|
-
const prevCount = guardRestartCounts.get(proc.name) || 0;
|
|
1428
|
-
const newCount = prevCount + 1;
|
|
1429
|
-
guardRestartCounts.set(proc.name, newCount);
|
|
1430
|
-
try {
|
|
1431
|
-
addHistoryEntry(proc.name, "restart", undefined, { by: "guard", count: newCount });
|
|
1432
|
-
} catch {}
|
|
1433
|
-
guardEvents.unshift({ time: now, name: proc.name, action: "restart", success: true });
|
|
1434
|
-
if (guardEvents.length > 100)
|
|
1435
|
-
guardEvents.pop();
|
|
1436
|
-
if (newCount > 5) {
|
|
1437
|
-
const backoffSeconds = Math.min(30 * Math.pow(2, newCount - 6), 300);
|
|
1438
|
-
guardNextRestartTime.set(proc.name, Date.now() + backoffSeconds * 1000);
|
|
1439
|
-
console.log(`[guard] \u2713 Restarted "${proc.name}" (restart #${newCount}). Crash loop detected: next check delayed by ${backoffSeconds}s.`);
|
|
1440
|
-
} else {
|
|
1441
|
-
console.log(`[guard] \u2713 Restarted "${proc.name}" (restart #${newCount})`);
|
|
1442
|
-
}
|
|
1812
|
+
return await parseConfigFile(fullConfigPath);
|
|
1443
1813
|
} catch (err) {
|
|
1444
|
-
console.
|
|
1445
|
-
|
|
1446
|
-
if (guardEvents.length > 100)
|
|
1447
|
-
guardEvents.pop();
|
|
1448
|
-
}
|
|
1449
|
-
} else {
|
|
1450
|
-
const prevCount = guardRestartCounts.get(proc.name) || 0;
|
|
1451
|
-
if (prevCount > 0) {
|
|
1452
|
-
const nextRestart = guardNextRestartTime.get(proc.name) || 0;
|
|
1453
|
-
if (Date.now() > nextRestart + 60000) {
|
|
1454
|
-
guardRestartCounts.delete(proc.name);
|
|
1455
|
-
guardNextRestartTime.delete(proc.name);
|
|
1456
|
-
}
|
|
1814
|
+
console.warn(`Warning: Failed to parse config file ${finalConfigPath}: ${err.message}`);
|
|
1815
|
+
return null;
|
|
1457
1816
|
}
|
|
1817
|
+
});
|
|
1818
|
+
if (configEnv) {
|
|
1819
|
+
finalEnv = { ...finalEnv, ...configEnv };
|
|
1820
|
+
console.log(`Loaded config from ${finalConfigPath}`);
|
|
1458
1821
|
}
|
|
1822
|
+
} else {
|
|
1823
|
+
console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
|
|
1459
1824
|
}
|
|
1460
|
-
} catch (err) {
|
|
1461
|
-
console.error(`[guard] Error in guard loop: ${err.message}`);
|
|
1462
1825
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
// src/guard.ts
|
|
1485
|
-
var exports_guard = {};
|
|
1486
|
-
__export(exports_guard, {
|
|
1487
|
-
startGuardLoop: () => startGuardLoop
|
|
1488
|
-
});
|
|
1489
|
-
import { createHmac } from "crypto";
|
|
1490
|
-
async function notifyWebhook(event, name, details) {
|
|
1491
|
-
if (!WEBHOOK_URL)
|
|
1492
|
-
return;
|
|
1493
|
-
try {
|
|
1494
|
-
const payload = JSON.stringify({
|
|
1495
|
-
event,
|
|
1496
|
-
process: name,
|
|
1497
|
-
timestamp: new Date().toISOString(),
|
|
1498
|
-
...details
|
|
1499
|
-
});
|
|
1500
|
-
const headers = {
|
|
1501
|
-
"Content-Type": "application/json",
|
|
1502
|
-
"User-Agent": "bgrun-guard/1.0"
|
|
1503
|
-
};
|
|
1504
|
-
if (WEBHOOK_SECRET) {
|
|
1505
|
-
const sig = createHmac("sha256", WEBHOOK_SECRET).update(payload).digest("hex");
|
|
1506
|
-
headers["X-BGR-Signature"] = `sha256=${sig}`;
|
|
1507
|
-
}
|
|
1508
|
-
const controller = new AbortController;
|
|
1509
|
-
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
1510
|
-
await fetch(WEBHOOK_URL, {
|
|
1511
|
-
method: "POST",
|
|
1512
|
-
headers,
|
|
1513
|
-
body: payload,
|
|
1514
|
-
signal: controller.signal
|
|
1515
|
-
});
|
|
1516
|
-
clearTimeout(timeout);
|
|
1517
|
-
} catch (err) {
|
|
1518
|
-
console.error(`[guard] Webhook failed: ${err.message}`);
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
async function restartProcess(name) {
|
|
1522
|
-
try {
|
|
1523
|
-
await handleRun({
|
|
1524
|
-
action: "run",
|
|
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,
|
|
1525
1847
|
name,
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
function getBackoffMs(restartCount) {
|
|
1536
|
-
if (restartCount <= CRASH_THRESHOLD)
|
|
1537
|
-
return 0;
|
|
1538
|
-
const exponent = restartCount - CRASH_THRESHOLD;
|
|
1539
|
-
return Math.min(30000 * Math.pow(2, exponent - 1), MAX_BACKOFF_MS);
|
|
1540
|
-
}
|
|
1541
|
-
async function guardCycle() {
|
|
1542
|
-
try {
|
|
1543
|
-
const processes = getAllProcesses();
|
|
1544
|
-
if (processes.length === 0)
|
|
1545
|
-
return;
|
|
1546
|
-
const now = Date.now();
|
|
1547
|
-
let checked = 0;
|
|
1548
|
-
let restarted = 0;
|
|
1549
|
-
let skipped = 0;
|
|
1550
|
-
for (const proc of processes) {
|
|
1551
|
-
if (proc.name === "bgr-guard")
|
|
1552
|
-
continue;
|
|
1553
|
-
const env = proc.env ? parseEnvString(proc.env) : {};
|
|
1554
|
-
const isGuarded = env.BGR_KEEP_ALIVE === "true";
|
|
1555
|
-
const isDashboard = proc.name === "bgr-dashboard";
|
|
1556
|
-
if (!isGuarded && !isDashboard)
|
|
1557
|
-
continue;
|
|
1558
|
-
checked++;
|
|
1559
|
-
try {
|
|
1560
|
-
const alive = await isProcessRunning(proc.pid, proc.command);
|
|
1561
|
-
if (!alive && proc.pid > 0) {
|
|
1562
|
-
const nextRestart = state.nextRestartTime.get(proc.name) || 0;
|
|
1563
|
-
if (now < nextRestart) {
|
|
1564
|
-
const waitSecs = Math.round((nextRestart - now) / 1000);
|
|
1565
|
-
skipped++;
|
|
1566
|
-
continue;
|
|
1567
|
-
}
|
|
1568
|
-
console.log(`[guard] \u26A0 "${proc.name}" (PID ${proc.pid}) is dead \u2014 restarting...`);
|
|
1569
|
-
notifyWebhook("crash", proc.name, { pid: proc.pid, isDashboard });
|
|
1570
|
-
const success = await restartProcess(proc.name);
|
|
1571
|
-
if (success) {
|
|
1572
|
-
const count = (state.restartCounts.get(proc.name) || 0) + 1;
|
|
1573
|
-
state.restartCounts.set(proc.name, count);
|
|
1574
|
-
state.lastSeenAlive.delete(proc.name);
|
|
1575
|
-
const backoff = getBackoffMs(count);
|
|
1576
|
-
if (backoff > 0) {
|
|
1577
|
-
state.nextRestartTime.set(proc.name, now + backoff);
|
|
1578
|
-
console.log(`[guard] \u2713 Restarted "${proc.name}" (#${count}). Crash loop: next check in ${Math.round(backoff / 1000)}s`);
|
|
1579
|
-
} else {
|
|
1580
|
-
console.log(`[guard] \u2713 Restarted "${proc.name}" (#${count})`);
|
|
1581
|
-
}
|
|
1582
|
-
restarted++;
|
|
1583
|
-
notifyWebhook("restart", proc.name, { pid: proc.pid, restartCount: count, backoffMs: backoff });
|
|
1584
|
-
} else {
|
|
1585
|
-
notifyWebhook("restart_failed", proc.name, { pid: proc.pid });
|
|
1586
|
-
}
|
|
1587
|
-
} else if (alive) {
|
|
1588
|
-
const count = state.restartCounts.get(proc.name) || 0;
|
|
1589
|
-
if (count > 0) {
|
|
1590
|
-
const lastSeen = state.lastSeenAlive.get(proc.name);
|
|
1591
|
-
if (!lastSeen) {
|
|
1592
|
-
state.lastSeenAlive.set(proc.name, now);
|
|
1593
|
-
} else if (now - lastSeen > STABILITY_WINDOW_MS) {
|
|
1594
|
-
state.restartCounts.delete(proc.name);
|
|
1595
|
-
state.nextRestartTime.delete(proc.name);
|
|
1596
|
-
state.lastSeenAlive.delete(proc.name);
|
|
1597
|
-
console.log(`[guard] \u2713 "${proc.name}" stable for ${Math.round(STABILITY_WINDOW_MS / 1000)}s \u2014 reset counters`);
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
} catch (err) {
|
|
1602
|
-
console.error(`[guard] Error checking "${proc.name}": ${err.message}`);
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
if (restarted > 0) {
|
|
1606
|
-
console.log(`[guard] Cycle: ${checked} checked, ${restarted} restarted, ${skipped} in backoff`);
|
|
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);
|
|
1607
1855
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1856
|
+
announce(`${existingProcess ? "\uD83D\uDD04 Restarted" : "\uD83D\uDE80 Launched"} process "${name}" with PID ${actualPid}`, "Process Started");
|
|
1857
|
+
} finally {
|
|
1858
|
+
releaseOperationLock();
|
|
1610
1859
|
}
|
|
1611
1860
|
}
|
|
1612
|
-
async function startGuardLoop(intervalMs = DEFAULT_INTERVAL_MS) {
|
|
1613
|
-
const interval = intervalMs || DEFAULT_INTERVAL_MS;
|
|
1614
|
-
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`);
|
|
1615
|
-
console.log(`[guard] \uD83D\uDEE1\uFE0F BGR Standalone Guard started`);
|
|
1616
|
-
console.log(`[guard] Check interval: ${interval / 1000}s`);
|
|
1617
|
-
console.log(`[guard] Crash backoff threshold: ${CRASH_THRESHOLD} restarts`);
|
|
1618
|
-
console.log(`[guard] Stability window: ${STABILITY_WINDOW_MS / 1000}s`);
|
|
1619
|
-
console.log(`[guard] Monitoring: BGR_KEEP_ALIVE=true + bgr-dashboard`);
|
|
1620
|
-
console.log(`[guard] Webhook: ${WEBHOOK_URL || "(none \u2014 set BGR_WEBHOOK_URL to enable)"}`);
|
|
1621
|
-
console.log(`[guard] Started: ${new Date().toLocaleString()}`);
|
|
1622
|
-
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`);
|
|
1623
|
-
await guardCycle();
|
|
1624
|
-
setInterval(guardCycle, interval);
|
|
1625
|
-
}
|
|
1626
|
-
var WEBHOOK_URL, WEBHOOK_SECRET, DEFAULT_INTERVAL_MS = 30000, MAX_BACKOFF_MS, CRASH_THRESHOLD = 5, STABILITY_WINDOW_MS = 120000, state;
|
|
1627
|
-
var init_guard = __esm(() => {
|
|
1628
|
-
init_db();
|
|
1629
|
-
init_platform();
|
|
1630
|
-
init_run();
|
|
1631
|
-
init_utils();
|
|
1632
|
-
WEBHOOK_URL = process.env.BGR_WEBHOOK_URL || "";
|
|
1633
|
-
WEBHOOK_SECRET = process.env.BGR_WEBHOOK_SECRET || "";
|
|
1634
|
-
MAX_BACKOFF_MS = 5 * 60000;
|
|
1635
|
-
state = {
|
|
1636
|
-
restartCounts: new Map,
|
|
1637
|
-
nextRestartTime: new Map,
|
|
1638
|
-
lastSeenAlive: new Map
|
|
1639
|
-
};
|
|
1640
|
-
});
|
|
1641
|
-
|
|
1642
|
-
// src/index.ts
|
|
1643
|
-
init_utils();
|
|
1644
|
-
init_run();
|
|
1645
|
-
import { parseArgs } from "util";
|
|
1646
1861
|
|
|
1647
1862
|
// src/commands/list.ts
|
|
1648
1863
|
import chalk3 from "chalk";
|
|
@@ -1812,7 +2027,6 @@ function renderProcessTable(processes, options) {
|
|
|
1812
2027
|
|
|
1813
2028
|
// src/commands/list.ts
|
|
1814
2029
|
init_db();
|
|
1815
|
-
init_logger();
|
|
1816
2030
|
init_utils();
|
|
1817
2031
|
init_platform();
|
|
1818
2032
|
function formatMemory(bytes) {
|
|
@@ -1826,6 +2040,8 @@ function formatMemory(bytes) {
|
|
|
1826
2040
|
async function showAll(opts) {
|
|
1827
2041
|
const processes = getAllProcesses();
|
|
1828
2042
|
const filtered = processes.filter((proc) => {
|
|
2043
|
+
if (isInternalProcessName(proc.name))
|
|
2044
|
+
return false;
|
|
1829
2045
|
if (!opts?.filter)
|
|
1830
2046
|
return true;
|
|
1831
2047
|
const envVars = parseEnvString(proc.env);
|
|
@@ -1873,11 +2089,21 @@ async function showAll(opts) {
|
|
|
1873
2089
|
for (const proc of filtered) {
|
|
1874
2090
|
const isRunning = aliveCache.get(proc.pid) ?? await isProcessRunning(proc.pid, proc.command);
|
|
1875
2091
|
const runtime = calculateRuntime(proc.timestamp);
|
|
1876
|
-
|
|
1877
|
-
|
|
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;
|
|
1878
2104
|
tableData.push({
|
|
1879
2105
|
id: proc.id,
|
|
1880
|
-
pid:
|
|
2106
|
+
pid: displayPid,
|
|
1881
2107
|
name: proc.name,
|
|
1882
2108
|
port: ports.length > 0 ? ports.map((p) => `:${p}`).join(",") : "-",
|
|
1883
2109
|
memory: formatMemory(mem),
|
|
@@ -1909,7 +2135,7 @@ async function showAll(opts) {
|
|
|
1909
2135
|
// src/commands/cleanup.ts
|
|
1910
2136
|
init_db();
|
|
1911
2137
|
init_platform();
|
|
1912
|
-
|
|
2138
|
+
init_utils();
|
|
1913
2139
|
import * as fs3 from "fs";
|
|
1914
2140
|
async function handleDelete(name) {
|
|
1915
2141
|
const process2 = getProcess(name);
|
|
@@ -1921,6 +2147,9 @@ async function handleDelete(name) {
|
|
|
1921
2147
|
if (isRunning) {
|
|
1922
2148
|
await terminateProcess(process2.pid);
|
|
1923
2149
|
}
|
|
2150
|
+
if (!isInternalProcessName(name)) {
|
|
2151
|
+
await stopProcessWatcher(name);
|
|
2152
|
+
}
|
|
1924
2153
|
if (fs3.existsSync(process2.stdout_path)) {
|
|
1925
2154
|
try {
|
|
1926
2155
|
fs3.unlinkSync(process2.stdout_path);
|
|
@@ -1941,6 +2170,12 @@ async function handleClean() {
|
|
|
1941
2170
|
for (const proc of processes) {
|
|
1942
2171
|
const running = await isProcessRunning(proc.pid);
|
|
1943
2172
|
if (!running) {
|
|
2173
|
+
const watched = getWatchedProcessName(proc.name);
|
|
2174
|
+
if (watched) {
|
|
2175
|
+
removeProcess(proc.pid);
|
|
2176
|
+
cleanedCount++;
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
1944
2179
|
removeProcess(proc.pid);
|
|
1945
2180
|
cleanedCount++;
|
|
1946
2181
|
if (fs3.existsSync(proc.stdout_path)) {
|
|
@@ -1969,18 +2204,33 @@ async function handleStop(name) {
|
|
|
1969
2204
|
error(`No process found named '${name}'`);
|
|
1970
2205
|
return;
|
|
1971
2206
|
}
|
|
1972
|
-
const
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
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();
|
|
1981
2233
|
}
|
|
1982
|
-
updateProcessPid(name, 0);
|
|
1983
|
-
announce(`Process '${name}' has been stopped (kept in registry).`, "Process Stopped");
|
|
1984
2234
|
}
|
|
1985
2235
|
async function handleDeleteAll() {
|
|
1986
2236
|
const processes = getAllProcesses();
|
|
@@ -1991,6 +2241,9 @@ async function handleDeleteAll() {
|
|
|
1991
2241
|
let killedCount = 0;
|
|
1992
2242
|
let portsFreed = 0;
|
|
1993
2243
|
for (const proc of processes) {
|
|
2244
|
+
if (!isInternalProcessName(proc.name)) {
|
|
2245
|
+
await stopProcessWatcher(proc.name);
|
|
2246
|
+
}
|
|
1994
2247
|
const running = await isProcessRunning(proc.pid);
|
|
1995
2248
|
if (running) {
|
|
1996
2249
|
const ports = await getProcessPorts(proc.pid);
|
|
@@ -2006,6 +2259,18 @@ async function handleDeleteAll() {
|
|
|
2006
2259
|
portsFreed++;
|
|
2007
2260
|
}
|
|
2008
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
|
+
}
|
|
2009
2274
|
if (fs3.existsSync(proc.stdout_path)) {
|
|
2010
2275
|
try {
|
|
2011
2276
|
fs3.unlinkSync(proc.stdout_path);
|
|
@@ -2029,9 +2294,7 @@ async function handleDeleteAll() {
|
|
|
2029
2294
|
// src/commands/watch.ts
|
|
2030
2295
|
init_db();
|
|
2031
2296
|
init_platform();
|
|
2032
|
-
init_logger();
|
|
2033
2297
|
init_utils();
|
|
2034
|
-
init_run();
|
|
2035
2298
|
import * as fs4 from "fs";
|
|
2036
2299
|
import path2 from "path";
|
|
2037
2300
|
import chalk4 from "chalk";
|
|
@@ -2221,7 +2484,6 @@ SIGINT received...`));
|
|
|
2221
2484
|
|
|
2222
2485
|
// src/commands/logs.ts
|
|
2223
2486
|
init_db();
|
|
2224
|
-
init_logger();
|
|
2225
2487
|
init_platform();
|
|
2226
2488
|
import chalk5 from "chalk";
|
|
2227
2489
|
import * as fs5 from "fs";
|
|
@@ -2266,7 +2528,6 @@ async function showLogs(name, logType = "both", lines) {
|
|
|
2266
2528
|
}
|
|
2267
2529
|
|
|
2268
2530
|
// src/commands/details.ts
|
|
2269
|
-
init_logger();
|
|
2270
2531
|
init_db();
|
|
2271
2532
|
init_utils();
|
|
2272
2533
|
init_platform();
|
|
@@ -2277,6 +2538,10 @@ async function showDetails(name) {
|
|
|
2277
2538
|
error(`No process found named '${name}'`);
|
|
2278
2539
|
return;
|
|
2279
2540
|
}
|
|
2541
|
+
if (isInternalProcessName(proc.name)) {
|
|
2542
|
+
error(`'${name}' is an internal bgrun process.`);
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
2280
2545
|
let isRunning = await isProcessRunning(proc.pid, proc.command);
|
|
2281
2546
|
if (!isRunning && proc.pid > 0) {
|
|
2282
2547
|
const reconciled = await reconcileProcessPids([{ name: proc.name, pid: proc.pid, command: proc.command, workdir: proc.workdir }], new Set([proc.pid]));
|
|
@@ -2289,7 +2554,15 @@ async function showDetails(name) {
|
|
|
2289
2554
|
}
|
|
2290
2555
|
const runtime = calculateRuntime(proc.timestamp);
|
|
2291
2556
|
const envVars = parseEnvString(proc.env);
|
|
2292
|
-
|
|
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
|
+
}
|
|
2293
2566
|
const portDisplay = ports.length > 0 ? ports.map((p) => chalk6.hex("#FF6B6B")(`:${p}`)).join(", ") : null;
|
|
2294
2567
|
const details = `
|
|
2295
2568
|
${chalk6.bold("Process Details:")}
|
|
@@ -2313,13 +2586,225 @@ ${Object.entries(envVars).map(([key, value]) => `${chalk6.cyan.bold(key)} = ${ch
|
|
|
2313
2586
|
announce(details, `Process Details: ${name}`);
|
|
2314
2587
|
}
|
|
2315
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
|
+
|
|
2316
2802
|
// src/index.ts
|
|
2317
|
-
init_logger();
|
|
2318
2803
|
init_platform();
|
|
2319
2804
|
init_db();
|
|
2320
2805
|
import dedent from "dedent";
|
|
2321
2806
|
import chalk7 from "chalk";
|
|
2322
|
-
import { join as
|
|
2807
|
+
import { join as join6 } from "path";
|
|
2323
2808
|
var {sleep: sleep3 } = globalThis.Bun;
|
|
2324
2809
|
import { configure } from "measure-fn";
|
|
2325
2810
|
if (!Bun.argv.includes("--_serve")) {
|
|
@@ -2367,32 +2852,49 @@ function redirectConsoleToFiles() {
|
|
|
2367
2852
|
};
|
|
2368
2853
|
}
|
|
2369
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
|
+
}
|
|
2370
2866
|
async function showHelp() {
|
|
2371
2867
|
const usage = dedent`
|
|
2372
2868
|
${chalk7.bold("bgrun \u2014 Bun Background Runner")}
|
|
2373
2869
|
${chalk7.gray("\u2550".repeat(50))}
|
|
2374
2870
|
|
|
2375
2871
|
${chalk7.yellow("Usage:")}
|
|
2376
|
-
bgrun [name] [options]
|
|
2872
|
+
bunx bgrun [name] [options]
|
|
2377
2873
|
|
|
2378
2874
|
${chalk7.yellow("Commands:")}
|
|
2379
|
-
bgrun List all processes
|
|
2380
|
-
bgrun [name] Show details for a process
|
|
2381
|
-
bgrun --
|
|
2382
|
-
bgrun --
|
|
2383
|
-
bgrun --
|
|
2384
|
-
bgrun --
|
|
2385
|
-
bgrun
|
|
2386
|
-
bgrun --
|
|
2387
|
-
bgrun --
|
|
2388
|
-
bgrun --
|
|
2389
|
-
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
|
|
2390
2890
|
|
|
2391
2891
|
${chalk7.yellow("Options:")}
|
|
2392
2892
|
--name <string> Process name (required for new)
|
|
2393
2893
|
--command <string> Process command (required for new)
|
|
2394
2894
|
--directory <path> Working directory (required for new)
|
|
2395
2895
|
--config <path> Config file (default: .config.toml)
|
|
2896
|
+
--env Print shell export commands from config and exit
|
|
2897
|
+
--shell <type> Shell for --env: powershell | cmd | sh | json
|
|
2396
2898
|
--watch Watch for file changes and auto-restart
|
|
2397
2899
|
--force Force restart existing process
|
|
2398
2900
|
--fetch Fetch latest git changes before running
|
|
@@ -2405,85 +2907,187 @@ async function showHelp() {
|
|
|
2405
2907
|
--version Show version
|
|
2406
2908
|
--debug Show debug info (DB path, BGR home, etc.)
|
|
2407
2909
|
--dashboard Launch web dashboard as bgrun-managed process
|
|
2910
|
+
--guard Enable per-process crash watcher
|
|
2911
|
+
--guard-off Disable per-process crash watcher
|
|
2408
2912
|
--port <number> Port for dashboard (default: 3000)
|
|
2409
2913
|
--help Show this help message
|
|
2410
2914
|
|
|
2411
2915
|
${chalk7.yellow("Examples:")}
|
|
2412
|
-
bgrun --
|
|
2413
|
-
bgrun --
|
|
2414
|
-
bgrun
|
|
2916
|
+
bunx bgrun -- bun run dev
|
|
2917
|
+
bunx bgrun --force -- bun run server.ts
|
|
2918
|
+
bunx bgrun inline -- bun run dev
|
|
2919
|
+
Invoke-Expression (bunx bgrun --env)
|
|
2920
|
+
eval "$(bunx bgrun --env --shell sh)"
|
|
2921
|
+
bunx bgrun --dashboard
|
|
2922
|
+
bunx bgrun myapp --guard
|
|
2923
|
+
bunx bgrun myapp --guard-off
|
|
2924
|
+
bunx bgrun --name myapp --command "bun run dev" --directory . --watch
|
|
2925
|
+
bunx bgrun myapp --logs --lines 50
|
|
2415
2926
|
`;
|
|
2416
2927
|
console.log(usage);
|
|
2417
2928
|
}
|
|
2929
|
+
var cliArgOptions = {
|
|
2930
|
+
name: { type: "string" },
|
|
2931
|
+
command: { type: "string" },
|
|
2932
|
+
directory: { type: "string" },
|
|
2933
|
+
config: { type: "string" },
|
|
2934
|
+
env: { type: "boolean" },
|
|
2935
|
+
shell: { type: "string" },
|
|
2936
|
+
watch: { type: "boolean" },
|
|
2937
|
+
force: { type: "boolean" },
|
|
2938
|
+
fetch: { type: "boolean" },
|
|
2939
|
+
delete: { type: "boolean" },
|
|
2940
|
+
nuke: { type: "boolean" },
|
|
2941
|
+
restart: { type: "boolean" },
|
|
2942
|
+
"restart-all": { type: "boolean" },
|
|
2943
|
+
stop: { type: "boolean" },
|
|
2944
|
+
"stop-all": { type: "boolean" },
|
|
2945
|
+
clean: { type: "boolean" },
|
|
2946
|
+
json: { type: "boolean" },
|
|
2947
|
+
logs: { type: "boolean" },
|
|
2948
|
+
"log-stdout": { type: "boolean" },
|
|
2949
|
+
"log-stderr": { type: "boolean" },
|
|
2950
|
+
lines: { type: "string" },
|
|
2951
|
+
filter: { type: "string" },
|
|
2952
|
+
version: { type: "boolean" },
|
|
2953
|
+
help: { type: "boolean" },
|
|
2954
|
+
db: { type: "string" },
|
|
2955
|
+
stdout: { type: "string" },
|
|
2956
|
+
stderr: { type: "string" },
|
|
2957
|
+
dashboard: { type: "boolean" },
|
|
2958
|
+
guard: { type: "boolean" },
|
|
2959
|
+
"guard-off": { type: "boolean" },
|
|
2960
|
+
debug: { type: "boolean" },
|
|
2961
|
+
_serve: { type: "boolean" },
|
|
2962
|
+
"_watch-process": { type: "string" },
|
|
2963
|
+
port: { type: "string" }
|
|
2964
|
+
};
|
|
2418
2965
|
async function run2() {
|
|
2966
|
+
const rawArgs = Bun.argv.slice(2);
|
|
2967
|
+
const isActionInvocation = (values2) => {
|
|
2968
|
+
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);
|
|
2969
|
+
};
|
|
2970
|
+
if (rawArgs[0] === "inline") {
|
|
2971
|
+
const parsed = parseInlineArgs(rawArgs.slice(1));
|
|
2972
|
+
if (parsed.help) {
|
|
2973
|
+
console.log(dedent`
|
|
2974
|
+
${chalk7.bold("bgrun inline")}
|
|
2975
|
+
${chalk7.gray("\u2500".repeat(40))}
|
|
2976
|
+
|
|
2977
|
+
Run a command in the current terminal with env vars loaded from a bgrun config file.
|
|
2978
|
+
|
|
2979
|
+
Usage:
|
|
2980
|
+
bunx bgrun inline [--directory <path>] [--config <path>] -- <command> [args...]
|
|
2981
|
+
|
|
2982
|
+
Examples:
|
|
2983
|
+
bunx bgrun inline -- bun run dev
|
|
2984
|
+
bunx bgrun inline --directory apps/api -- node server.js
|
|
2985
|
+
`);
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2988
|
+
await handleInline(parsed);
|
|
2989
|
+
return;
|
|
2990
|
+
}
|
|
2991
|
+
const delimiterIndex = rawArgs.indexOf("--");
|
|
2992
|
+
if (delimiterIndex !== -1 && delimiterIndex < rawArgs.length - 1) {
|
|
2993
|
+
const preArgs = rawArgs.slice(0, delimiterIndex);
|
|
2994
|
+
const commandArgs = rawArgs.slice(delimiterIndex + 1);
|
|
2995
|
+
const { values: values2 } = parseArgs({
|
|
2996
|
+
args: preArgs,
|
|
2997
|
+
options: cliArgOptions,
|
|
2998
|
+
strict: false,
|
|
2999
|
+
allowPositionals: true
|
|
3000
|
+
});
|
|
3001
|
+
const autoName = values2.name || generateAutoProcessName();
|
|
3002
|
+
const inlineCommand = joinCommandArgs(commandArgs);
|
|
3003
|
+
const directory = values2.directory || process.cwd();
|
|
3004
|
+
await handleRun({
|
|
3005
|
+
action: "run",
|
|
3006
|
+
name: autoName,
|
|
3007
|
+
command: inlineCommand,
|
|
3008
|
+
directory,
|
|
3009
|
+
configPath: values2.config,
|
|
3010
|
+
force: values2.force,
|
|
3011
|
+
fetch: values2.fetch,
|
|
3012
|
+
remoteName: "",
|
|
3013
|
+
dbPath: values2.db,
|
|
3014
|
+
stdout: values2.stdout,
|
|
3015
|
+
stderr: values2.stderr
|
|
3016
|
+
});
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
2419
3019
|
const { values, positionals } = parseArgs({
|
|
2420
|
-
args:
|
|
2421
|
-
options:
|
|
2422
|
-
name: { type: "string" },
|
|
2423
|
-
command: { type: "string" },
|
|
2424
|
-
directory: { type: "string" },
|
|
2425
|
-
config: { type: "string" },
|
|
2426
|
-
watch: { type: "boolean" },
|
|
2427
|
-
force: { type: "boolean" },
|
|
2428
|
-
fetch: { type: "boolean" },
|
|
2429
|
-
delete: { type: "boolean" },
|
|
2430
|
-
nuke: { type: "boolean" },
|
|
2431
|
-
restart: { type: "boolean" },
|
|
2432
|
-
"restart-all": { type: "boolean" },
|
|
2433
|
-
stop: { type: "boolean" },
|
|
2434
|
-
"stop-all": { type: "boolean" },
|
|
2435
|
-
clean: { type: "boolean" },
|
|
2436
|
-
json: { type: "boolean" },
|
|
2437
|
-
logs: { type: "boolean" },
|
|
2438
|
-
"log-stdout": { type: "boolean" },
|
|
2439
|
-
"log-stderr": { type: "boolean" },
|
|
2440
|
-
lines: { type: "string" },
|
|
2441
|
-
filter: { type: "string" },
|
|
2442
|
-
version: { type: "boolean" },
|
|
2443
|
-
help: { type: "boolean" },
|
|
2444
|
-
db: { type: "string" },
|
|
2445
|
-
stdout: { type: "string" },
|
|
2446
|
-
stderr: { type: "string" },
|
|
2447
|
-
dashboard: { type: "boolean" },
|
|
2448
|
-
guard: { type: "boolean" },
|
|
2449
|
-
debug: { type: "boolean" },
|
|
2450
|
-
_serve: { type: "boolean" },
|
|
2451
|
-
"_guard-loop": { type: "boolean" },
|
|
2452
|
-
port: { type: "string" }
|
|
2453
|
-
},
|
|
3020
|
+
args: rawArgs,
|
|
3021
|
+
options: cliArgOptions,
|
|
2454
3022
|
strict: false,
|
|
2455
3023
|
allowPositionals: true
|
|
2456
3024
|
});
|
|
3025
|
+
if (values.env) {
|
|
3026
|
+
if (positionals.length > 1) {
|
|
3027
|
+
error("Too many positional arguments for --env. Use --config <path> or pass a single config path.");
|
|
3028
|
+
}
|
|
3029
|
+
await handleEnvit({
|
|
3030
|
+
directory: values.directory,
|
|
3031
|
+
configPath: values.config || positionals[0],
|
|
3032
|
+
shell: values.shell
|
|
3033
|
+
});
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
if (positionals.length > 1 && !values.command && !isActionInvocation(values)) {
|
|
3037
|
+
const autoName = values.name || generateAutoProcessName();
|
|
3038
|
+
const implicitCommand = joinCommandArgs(positionals);
|
|
3039
|
+
const directory = values.directory || process.cwd();
|
|
3040
|
+
await handleRun({
|
|
3041
|
+
action: "run",
|
|
3042
|
+
name: autoName,
|
|
3043
|
+
command: implicitCommand,
|
|
3044
|
+
directory,
|
|
3045
|
+
configPath: values.config,
|
|
3046
|
+
force: values.force,
|
|
3047
|
+
fetch: values.fetch,
|
|
3048
|
+
remoteName: "",
|
|
3049
|
+
dbPath: values.db,
|
|
3050
|
+
stdout: values.stdout,
|
|
3051
|
+
stderr: values.stderr
|
|
3052
|
+
});
|
|
3053
|
+
return;
|
|
3054
|
+
}
|
|
2457
3055
|
if (values["_serve"]) {
|
|
2458
3056
|
redirectConsoleToFiles();
|
|
2459
|
-
const { startServer: startServer2 } = await
|
|
3057
|
+
const { startServer: startServer2 } = await init_server().then(() => exports_server);
|
|
2460
3058
|
await startServer2();
|
|
2461
3059
|
return;
|
|
2462
3060
|
}
|
|
2463
|
-
if (values["
|
|
3061
|
+
if (values["_watch-process"]) {
|
|
2464
3062
|
redirectConsoleToFiles();
|
|
2465
|
-
|
|
2466
|
-
const intervalStr = positionals[0];
|
|
2467
|
-
const intervalMs = intervalStr ? parseInt(intervalStr) * 1000 : undefined;
|
|
2468
|
-
await startGuardLoop2(intervalMs);
|
|
3063
|
+
await startProcessWatcher(String(values["_watch-process"]));
|
|
2469
3064
|
return;
|
|
2470
3065
|
}
|
|
2471
3066
|
if (values.dashboard) {
|
|
2472
3067
|
const dashboardName = "bgr-dashboard";
|
|
2473
3068
|
const homePath3 = getHomeDir();
|
|
2474
|
-
const bgrDir2 =
|
|
3069
|
+
const bgrDir2 = join6(homePath3, ".bgr");
|
|
2475
3070
|
const requestedPort = values.port;
|
|
3071
|
+
const explicitPortValue = requestedPort || Bun.env.BUN_PORT || undefined;
|
|
3072
|
+
const explicitPort = explicitPortValue ? parseInt(explicitPortValue, 10) : null;
|
|
2476
3073
|
const existing = getProcess(dashboardName);
|
|
2477
3074
|
if (existing && await isProcessRunning(existing.pid)) {
|
|
2478
|
-
|
|
3075
|
+
const resolved = await resolvePidWithPorts(existing.pid);
|
|
3076
|
+
let existingPid = resolved.pid;
|
|
3077
|
+
let existingPorts = resolved.ports;
|
|
2479
3078
|
if (existingPorts.length === 0) {
|
|
2480
|
-
const
|
|
2481
|
-
if (
|
|
2482
|
-
|
|
3079
|
+
const detachedPid = await findDetachedProcessByArg("--_serve");
|
|
3080
|
+
if (detachedPid && detachedPid !== existingPid) {
|
|
3081
|
+
const detachedResolved = await resolvePidWithPorts(detachedPid);
|
|
3082
|
+
existingPid = detachedResolved.pid;
|
|
3083
|
+
existingPorts = detachedResolved.ports;
|
|
2483
3084
|
}
|
|
2484
3085
|
}
|
|
3086
|
+
if (existingPid !== existing.pid && existingPorts.length > 0) {
|
|
3087
|
+
await retryDatabaseOperation(() => updateProcessPid(dashboardName, existingPid));
|
|
3088
|
+
}
|
|
2485
3089
|
const portStr = existingPorts.length > 0 ? `:${existingPorts[0]}` : "(detecting...)";
|
|
2486
|
-
announce(`Dashboard is already running (PID ${
|
|
3090
|
+
announce(`Dashboard is already running (PID ${existingPid})
|
|
2487
3091
|
|
|
2488
3092
|
` + ` \uD83C\uDF10 ${chalk7.cyan(`http://localhost${portStr}`)}
|
|
2489
3093
|
|
|
@@ -2502,29 +3106,28 @@ async function run2() {
|
|
|
2502
3106
|
}
|
|
2503
3107
|
await retryDatabaseOperation(() => removeProcessByName(dashboardName));
|
|
2504
3108
|
}
|
|
2505
|
-
const
|
|
2506
|
-
const
|
|
2507
|
-
const
|
|
2508
|
-
const
|
|
2509
|
-
const stdoutPath = join4(bgrDir2, `${dashboardName}-out.txt`);
|
|
2510
|
-
const stderrPath = join4(bgrDir2, `${dashboardName}-err.txt`);
|
|
3109
|
+
const spawnCommand = `bunx bgrun --_serve`;
|
|
3110
|
+
const command = `bunx bgrun --_serve`;
|
|
3111
|
+
const stdoutPath = join6(bgrDir2, `${dashboardName}-out.txt`);
|
|
3112
|
+
const stderrPath = join6(bgrDir2, `${dashboardName}-err.txt`);
|
|
2511
3113
|
await Bun.write(stdoutPath, "");
|
|
2512
3114
|
await Bun.write(stderrPath, "");
|
|
2513
3115
|
const spawnEnv = { ...Bun.env };
|
|
2514
|
-
if (
|
|
2515
|
-
spawnEnv.BUN_PORT =
|
|
3116
|
+
if (explicitPortValue) {
|
|
3117
|
+
spawnEnv.BUN_PORT = explicitPortValue;
|
|
3118
|
+
} else {
|
|
3119
|
+
delete spawnEnv.BUN_PORT;
|
|
2516
3120
|
}
|
|
2517
3121
|
spawnEnv.BGR_STDOUT = stdoutPath;
|
|
2518
3122
|
spawnEnv.BGR_STDERR = stderrPath;
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
const portFree = await isPortFree(targetPort);
|
|
3123
|
+
if (explicitPort && explicitPort > 0) {
|
|
3124
|
+
const portFree = await isPortFree(explicitPort);
|
|
2522
3125
|
if (!portFree) {
|
|
2523
|
-
console.log(chalk7.yellow(` \u26A1
|
|
2524
|
-
await killProcessOnPort(
|
|
2525
|
-
const freed = await waitForPortFree(
|
|
3126
|
+
console.log(chalk7.yellow(` \u26A1 Requested dashboard port ${explicitPort} is occupied \u2014 reclaiming...`));
|
|
3127
|
+
await killProcessOnPort(explicitPort);
|
|
3128
|
+
const freed = await waitForPortFree(explicitPort, 5000);
|
|
2526
3129
|
if (!freed) {
|
|
2527
|
-
console.log(chalk7.red(` \u26A0 Could not free port ${
|
|
3130
|
+
console.log(chalk7.red(` \u26A0 Could not free port ${explicitPort} \u2014 dashboard may pick a fallback port`));
|
|
2528
3131
|
}
|
|
2529
3132
|
}
|
|
2530
3133
|
}
|
|
@@ -2537,15 +3140,24 @@ async function run2() {
|
|
|
2537
3140
|
});
|
|
2538
3141
|
newProcess.unref();
|
|
2539
3142
|
await sleep3(2000);
|
|
2540
|
-
|
|
2541
|
-
|
|
3143
|
+
let actualPid = explicitPort && explicitPort > 0 ? await findPidByPort(explicitPort, 1e4) ?? await findChildPid(newProcess.pid) : await findChildPid(newProcess.pid);
|
|
3144
|
+
if (!await isProcessRunning(actualPid)) {
|
|
3145
|
+
const detachedPid = await findDetachedProcessByArg("--_serve");
|
|
3146
|
+
if (detachedPid)
|
|
3147
|
+
actualPid = detachedPid;
|
|
3148
|
+
}
|
|
2542
3149
|
let actualPort = null;
|
|
2543
3150
|
for (let attempt = 0;attempt < 10; attempt++) {
|
|
2544
|
-
const
|
|
2545
|
-
|
|
2546
|
-
|
|
3151
|
+
const resolved = await resolvePidWithPorts(actualPid);
|
|
3152
|
+
actualPid = resolved.pid;
|
|
3153
|
+
if (resolved.ports.length > 0) {
|
|
3154
|
+
actualPort = resolved.ports[0];
|
|
2547
3155
|
break;
|
|
2548
3156
|
}
|
|
3157
|
+
const detachedPid = await findDetachedProcessByArg("--_serve");
|
|
3158
|
+
if (detachedPid && detachedPid !== actualPid) {
|
|
3159
|
+
actualPid = detachedPid;
|
|
3160
|
+
}
|
|
2549
3161
|
await sleep3(1000);
|
|
2550
3162
|
}
|
|
2551
3163
|
await retryDatabaseOperation(() => insertProcess({
|
|
@@ -2578,75 +3190,8 @@ async function run2() {
|
|
|
2578
3190
|
announce(msg, "BGR Dashboard");
|
|
2579
3191
|
return;
|
|
2580
3192
|
}
|
|
2581
|
-
if (values.guard) {
|
|
2582
|
-
|
|
2583
|
-
const homePath3 = getHomeDir();
|
|
2584
|
-
const bgrDir2 = join4(homePath3, ".bgr");
|
|
2585
|
-
const existing = getProcess(guardName);
|
|
2586
|
-
if (existing && await isProcessRunning(existing.pid)) {
|
|
2587
|
-
announce(`Guard is already running (PID ${existing.pid})
|
|
2588
|
-
|
|
2589
|
-
Use ${chalk7.yellow(`bgrun --stop ${guardName}`)} to stop it
|
|
2590
|
-
Use ${chalk7.yellow(`bgrun --guard --force`)} to restart`, "BGR Guard");
|
|
2591
|
-
return;
|
|
2592
|
-
}
|
|
2593
|
-
if (existing) {
|
|
2594
|
-
if (await isProcessRunning(existing.pid)) {
|
|
2595
|
-
await terminateProcess(existing.pid);
|
|
2596
|
-
}
|
|
2597
|
-
await retryDatabaseOperation(() => removeProcessByName(guardName));
|
|
2598
|
-
}
|
|
2599
|
-
const { resolve } = __require("path");
|
|
2600
|
-
const scriptPath = resolve(process.argv[1]);
|
|
2601
|
-
const spawnCommand = `bun run ${scriptPath} --_guard-loop`;
|
|
2602
|
-
const command = `bgrun --_guard-loop`;
|
|
2603
|
-
const stdoutPath = join4(bgrDir2, `${guardName}-out.txt`);
|
|
2604
|
-
const stderrPath = join4(bgrDir2, `${guardName}-err.txt`);
|
|
2605
|
-
await Bun.write(stdoutPath, "");
|
|
2606
|
-
await Bun.write(stderrPath, "");
|
|
2607
|
-
const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
|
|
2608
|
-
env: { ...Bun.env, BGR_STDOUT: stdoutPath, BGR_STDERR: stderrPath },
|
|
2609
|
-
cwd: bgrDir2,
|
|
2610
|
-
stdout: "ignore",
|
|
2611
|
-
stderr: "ignore",
|
|
2612
|
-
detached: true
|
|
2613
|
-
});
|
|
2614
|
-
newProcess.unref();
|
|
2615
|
-
await sleep3(1000);
|
|
2616
|
-
let actualPid = await findChildPid(newProcess.pid);
|
|
2617
|
-
if (!await isProcessRunning(actualPid)) {
|
|
2618
|
-
const { psExec: ps } = await Promise.resolve().then(() => (init_platform(), exports_platform));
|
|
2619
|
-
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);
|
|
2620
|
-
const foundPid = parseInt(result.trim());
|
|
2621
|
-
if (!isNaN(foundPid) && foundPid > 0)
|
|
2622
|
-
actualPid = foundPid;
|
|
2623
|
-
}
|
|
2624
|
-
await retryDatabaseOperation(() => insertProcess({
|
|
2625
|
-
pid: actualPid,
|
|
2626
|
-
workdir: bgrDir2,
|
|
2627
|
-
command,
|
|
2628
|
-
name: guardName,
|
|
2629
|
-
env: "BGR_KEEP_ALIVE=false",
|
|
2630
|
-
configPath: "",
|
|
2631
|
-
stdout_path: stdoutPath,
|
|
2632
|
-
stderr_path: stderrPath
|
|
2633
|
-
}));
|
|
2634
|
-
const msg = dedent`
|
|
2635
|
-
${chalk7.bold("\uD83D\uDEE1\uFE0F BGR Standalone Guard launched")}
|
|
2636
|
-
${chalk7.gray("\u2500".repeat(40))}
|
|
2637
|
-
|
|
2638
|
-
Monitors: All processes with BGR_KEEP_ALIVE=true
|
|
2639
|
-
Also watches: bgr-dashboard (auto-restart if it dies)
|
|
2640
|
-
Check interval: 30 seconds
|
|
2641
|
-
Backoff: Exponential after 5 rapid crashes
|
|
2642
|
-
|
|
2643
|
-
${chalk7.gray("\u2500".repeat(40))}
|
|
2644
|
-
Process: ${chalk7.white(guardName)} | PID: ${chalk7.white(String(actualPid))}
|
|
2645
|
-
|
|
2646
|
-
${chalk7.yellow(`bgrun ${guardName} --logs`)} View guard logs
|
|
2647
|
-
${chalk7.yellow(`bgrun --stop ${guardName}`)} Stop the guard
|
|
2648
|
-
`;
|
|
2649
|
-
announce(msg, "BGR Guard");
|
|
3193
|
+
if (values.guard || values["guard-off"]) {
|
|
3194
|
+
await handleGuardToggle(positionals[0], Boolean(values.guard));
|
|
2650
3195
|
return;
|
|
2651
3196
|
}
|
|
2652
3197
|
if (values.version) {
|