bgrun 3.12.0 → 3.12.2
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/dashboard/app/api/check-port/route.ts +35 -0
- package/dashboard/app/api/dependencies/route.ts +40 -0
- package/dashboard/app/api/deploy/[name]/route.ts +6 -41
- package/dashboard/app/api/deploy-all/route.ts +25 -0
- package/dashboard/app/api/guard/route.ts +4 -1
- package/dashboard/app/api/guard-events/route.ts +5 -0
- package/dashboard/app/api/history/route.ts +39 -0
- package/dashboard/app/api/next-port/route.ts +32 -0
- package/dashboard/app/api/processes/route.ts +11 -3
- package/dashboard/app/api/restart/[name]/route.ts +7 -0
- package/dashboard/app/api/start/route.ts +11 -0
- package/dashboard/app/api/stop/[name]/route.ts +4 -1
- package/dashboard/app/api/templates/route.ts +47 -0
- package/dashboard/app/globals.css +1565 -5
- package/dashboard/app/page.client.tsx +1907 -2
- package/dashboard/app/page.tsx +292 -5
- package/dist/index.js +787 -194
- package/package.json +2 -2
- package/scripts/bgr-startup.ps1 +3 -3
- package/scripts/bgrun-startup.ps1 +91 -0
- package/src/bgrun.test.ts +171 -0
- package/src/commands/details.ts +17 -3
- package/src/commands/list.ts +37 -4
- package/src/commands/run.ts +21 -3
- package/src/db.ts +257 -0
- package/src/deploy.ts +163 -0
- package/src/guard.ts +51 -0
- package/src/index.ts +92 -14
- package/src/index_copy.ts +614 -0
- package/src/logger.ts +12 -2
- package/src/platform.ts +101 -56
- package/src/server.ts +87 -3
- package/src/utils.ts +2 -2
package/dist/index.js
CHANGED
|
@@ -1,23 +1,66 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
|
+
var __returnValue = (v) => v;
|
|
5
|
+
function __exportSetter(name, newValue) {
|
|
6
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
7
|
+
}
|
|
4
8
|
var __export = (target, all) => {
|
|
5
9
|
for (var name in all)
|
|
6
10
|
__defProp(target, name, {
|
|
7
11
|
get: all[name],
|
|
8
12
|
enumerable: true,
|
|
9
13
|
configurable: true,
|
|
10
|
-
set: (
|
|
14
|
+
set: __exportSetter.bind(all, name)
|
|
11
15
|
});
|
|
12
16
|
};
|
|
13
17
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
14
18
|
var __require = import.meta.require;
|
|
15
19
|
|
|
16
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
|
+
killProcessOnPort: () => killProcessOnPort,
|
|
29
|
+
isWindows: () => isWindows,
|
|
30
|
+
isProcessRunning: () => isProcessRunning,
|
|
31
|
+
isPortFree: () => isPortFree,
|
|
32
|
+
getShellCommand: () => getShellCommand,
|
|
33
|
+
getProcessPorts: () => getProcessPorts,
|
|
34
|
+
getProcessMemory: () => getProcessMemory,
|
|
35
|
+
getProcessBatchResources: () => getProcessBatchResources,
|
|
36
|
+
getPortInfo: () => getPortInfo,
|
|
37
|
+
getHomeDir: () => getHomeDir,
|
|
38
|
+
findPidByPort: () => findPidByPort,
|
|
39
|
+
findChildPid: () => findChildPid,
|
|
40
|
+
ensureDir: () => ensureDir,
|
|
41
|
+
copyFile: () => copyFile
|
|
42
|
+
});
|
|
17
43
|
import * as fs from "fs";
|
|
18
44
|
import * as os from "os";
|
|
45
|
+
import { join } from "path";
|
|
19
46
|
var {$ } = globalThis.Bun;
|
|
20
47
|
import { createMeasure } from "measure-fn";
|
|
48
|
+
function psExec(command, _timeoutMs = 3000) {
|
|
49
|
+
const tmpFile = join(os.tmpdir(), `bgr-ps-${Date.now()}.ps1`);
|
|
50
|
+
try {
|
|
51
|
+
fs.writeFileSync(tmpFile, command);
|
|
52
|
+
const result = Bun.spawnSync(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", tmpFile]);
|
|
53
|
+
try {
|
|
54
|
+
fs.unlinkSync(tmpFile);
|
|
55
|
+
} catch {}
|
|
56
|
+
return result.stdout?.toString() || "";
|
|
57
|
+
} catch {
|
|
58
|
+
try {
|
|
59
|
+
fs.unlinkSync(tmpFile);
|
|
60
|
+
} catch {}
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
21
64
|
function isWindows() {
|
|
22
65
|
return process.platform === "win32";
|
|
23
66
|
}
|
|
@@ -33,8 +76,13 @@ async function isProcessRunning(pid, command) {
|
|
|
33
76
|
return await isDockerContainerRunning(command);
|
|
34
77
|
}
|
|
35
78
|
if (isWindows()) {
|
|
36
|
-
|
|
37
|
-
|
|
79
|
+
try {
|
|
80
|
+
process.kill(pid, 0);
|
|
81
|
+
return true;
|
|
82
|
+
} catch {
|
|
83
|
+
const output = psExec(`Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`).trim();
|
|
84
|
+
return output === String(pid);
|
|
85
|
+
}
|
|
38
86
|
} else {
|
|
39
87
|
const result = await $`ps -p ${pid}`.nothrow().text();
|
|
40
88
|
return result.includes(`${pid}`);
|
|
@@ -66,7 +114,7 @@ async function isDockerContainerRunning(command) {
|
|
|
66
114
|
async function getChildPids(pid) {
|
|
67
115
|
try {
|
|
68
116
|
if (isWindows()) {
|
|
69
|
-
const result = await
|
|
117
|
+
const result = await psExec(`Get-CimInstance Win32_Process -Filter 'ParentProcessId=${pid}' | Select-Object -ExpandProperty ProcessId`, 3000);
|
|
70
118
|
return result.split(`
|
|
71
119
|
`).map((line) => parseInt(line.trim())).filter((n) => !isNaN(n) && n > 0);
|
|
72
120
|
} else {
|
|
@@ -125,6 +173,35 @@ async function isPortFree(port) {
|
|
|
125
173
|
return true;
|
|
126
174
|
}
|
|
127
175
|
}
|
|
176
|
+
async function getPortInfo(port) {
|
|
177
|
+
try {
|
|
178
|
+
if (isWindows()) {
|
|
179
|
+
const result = await $`netstat -ano | findstr :${port}`.nothrow().quiet().text();
|
|
180
|
+
for (const line of result.split(`
|
|
181
|
+
`)) {
|
|
182
|
+
const match = line.match(new RegExp(`:(${port})\\s+.*LISTENING\\s+(\\d+)`));
|
|
183
|
+
if (match) {
|
|
184
|
+
const pid = parseInt(match[2]);
|
|
185
|
+
if (pid > 0 && await isProcessRunning(pid)) {
|
|
186
|
+
const nameResult = await $`powershell -NoProfile -Command "(Get-Process -Id ${pid} -ErrorAction SilentlyContinue).ProcessName"`.nothrow().quiet().text();
|
|
187
|
+
return { inUse: true, pid, processName: nameResult.trim() || "unknown" };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return { inUse: false };
|
|
192
|
+
} else {
|
|
193
|
+
const result = await $`ss -tln sport = :${port}`.nothrow().quiet().text();
|
|
194
|
+
const lines = result.trim().split(`
|
|
195
|
+
`).filter((l) => l.trim());
|
|
196
|
+
if (lines.length > 1) {
|
|
197
|
+
return { inUse: true };
|
|
198
|
+
}
|
|
199
|
+
return { inUse: false };
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
return { inUse: false };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
128
205
|
async function waitForPortFree(port, timeoutMs = 5000) {
|
|
129
206
|
const startTime = Date.now();
|
|
130
207
|
const pollInterval = 300;
|
|
@@ -188,12 +265,12 @@ function getShellCommand(command) {
|
|
|
188
265
|
}
|
|
189
266
|
async function findChildPid(parentPid) {
|
|
190
267
|
let currentPid = parentPid;
|
|
191
|
-
const maxDepth =
|
|
268
|
+
const maxDepth = 2;
|
|
192
269
|
for (let depth = 0;depth < maxDepth; depth++) {
|
|
193
270
|
try {
|
|
194
271
|
let childPids = [];
|
|
195
272
|
if (isWindows()) {
|
|
196
|
-
const result = await
|
|
273
|
+
const result = await psExec(`Get-CimInstance Win32_Process -Filter 'ParentProcessId=${currentPid}' | Select-Object -ExpandProperty ProcessId`, 3000);
|
|
197
274
|
childPids = result.split(`
|
|
198
275
|
`).map((line) => parseInt(line.trim())).filter((n) => !isNaN(n) && n > 0);
|
|
199
276
|
} else {
|
|
@@ -201,9 +278,8 @@ async function findChildPid(parentPid) {
|
|
|
201
278
|
childPids = result.trim().split(`
|
|
202
279
|
`).map((line) => parseInt(line.trim())).filter((n) => !isNaN(n) && n > 0);
|
|
203
280
|
}
|
|
204
|
-
if (childPids.length === 0)
|
|
281
|
+
if (childPids.length === 0)
|
|
205
282
|
break;
|
|
206
|
-
}
|
|
207
283
|
currentPid = childPids[0];
|
|
208
284
|
} catch {
|
|
209
285
|
break;
|
|
@@ -211,6 +287,112 @@ async function findChildPid(parentPid) {
|
|
|
211
287
|
}
|
|
212
288
|
return currentPid;
|
|
213
289
|
}
|
|
290
|
+
async function reconcileProcessPids(processes, deadPids) {
|
|
291
|
+
return await plat.measure("Reconcile PIDs", async () => {
|
|
292
|
+
const result = new Map;
|
|
293
|
+
const needsReconciliation = processes.filter((p) => deadPids.has(p.pid) && p.pid > 0);
|
|
294
|
+
if (needsReconciliation.length === 0)
|
|
295
|
+
return result;
|
|
296
|
+
try {
|
|
297
|
+
let runningProcs = [];
|
|
298
|
+
if (isWindows()) {
|
|
299
|
+
const output = await psExec(`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | ForEach-Object { Write-Output "$($_.ProcessId)|$($_.CommandLine)" }`, 5000);
|
|
300
|
+
for (const line of output.split(`
|
|
301
|
+
`)) {
|
|
302
|
+
const sepIdx = line.indexOf("|");
|
|
303
|
+
if (sepIdx === -1)
|
|
304
|
+
continue;
|
|
305
|
+
const pid = parseInt(line.substring(0, sepIdx).trim());
|
|
306
|
+
const cmdLine = line.substring(sepIdx + 1).trim();
|
|
307
|
+
if (!isNaN(pid) && pid > 0 && cmdLine) {
|
|
308
|
+
runningProcs.push({ pid, cmdLine });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
const psOutput = await $`ps -eo pid,args --no-headers`.nothrow().quiet().text();
|
|
313
|
+
for (const line of psOutput.trim().split(`
|
|
314
|
+
`)) {
|
|
315
|
+
const match = line.trim().match(/^(\d+)\s+(.+)/);
|
|
316
|
+
if (match) {
|
|
317
|
+
runningProcs.push({ pid: parseInt(match[1]), cmdLine: match[2] });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
for (const proc of needsReconciliation) {
|
|
322
|
+
const cmdParts = proc.command.split(/\s+/);
|
|
323
|
+
const workdirParts = proc.workdir.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
324
|
+
const workdirLast = workdirParts[workdirParts.length - 1]?.toLowerCase() || "";
|
|
325
|
+
let bestMatch = null;
|
|
326
|
+
let ambiguous = false;
|
|
327
|
+
for (const running of runningProcs) {
|
|
328
|
+
const cmdLower = running.cmdLine.toLowerCase();
|
|
329
|
+
let score = 0;
|
|
330
|
+
for (const part of cmdParts) {
|
|
331
|
+
if (part.length > 2 && cmdLower.includes(part.toLowerCase()))
|
|
332
|
+
score++;
|
|
333
|
+
}
|
|
334
|
+
if (workdirLast && cmdLower.includes(workdirLast))
|
|
335
|
+
score += 3;
|
|
336
|
+
if (cmdLower.includes(proc.workdir.toLowerCase().replace(/\\/g, "/")))
|
|
337
|
+
score += 5;
|
|
338
|
+
if (cmdLower.includes(proc.workdir.toLowerCase()))
|
|
339
|
+
score += 5;
|
|
340
|
+
if (score < 4)
|
|
341
|
+
continue;
|
|
342
|
+
if (!bestMatch || score > bestMatch.score) {
|
|
343
|
+
ambiguous = false;
|
|
344
|
+
bestMatch = { pid: running.pid, score };
|
|
345
|
+
} else if (score === bestMatch.score) {
|
|
346
|
+
ambiguous = true;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (bestMatch && !ambiguous) {
|
|
350
|
+
result.set(proc.name, bestMatch.pid);
|
|
351
|
+
runningProcs = runningProcs.filter((p) => p.pid !== bestMatch.pid);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
} catch {}
|
|
355
|
+
return result;
|
|
356
|
+
}) ?? new Map;
|
|
357
|
+
}
|
|
358
|
+
async function findPidByPort(port, maxWaitMs = 8000) {
|
|
359
|
+
const start = Date.now();
|
|
360
|
+
const pollMs = 500;
|
|
361
|
+
while (Date.now() - start < maxWaitMs) {
|
|
362
|
+
try {
|
|
363
|
+
if (isWindows()) {
|
|
364
|
+
const result = await $`netstat -ano`.nothrow().quiet().text();
|
|
365
|
+
for (const line of result.split(`
|
|
366
|
+
`)) {
|
|
367
|
+
if (line.includes(`:${port}`) && line.includes("LISTENING")) {
|
|
368
|
+
const parts = line.trim().split(/\s+/);
|
|
369
|
+
const pid = parseInt(parts[parts.length - 1]);
|
|
370
|
+
if (!isNaN(pid) && pid > 0)
|
|
371
|
+
return pid;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
try {
|
|
376
|
+
const result2 = await $`ss -tlnp`.nothrow().quiet().text();
|
|
377
|
+
for (const line of result2.split(`
|
|
378
|
+
`)) {
|
|
379
|
+
if (line.includes(`:${port}`)) {
|
|
380
|
+
const pidMatch = line.match(/pid=(\d+)/);
|
|
381
|
+
if (pidMatch)
|
|
382
|
+
return parseInt(pidMatch[1]);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
} catch {}
|
|
386
|
+
const result = await $`lsof -iTCP:${port} -sTCP:LISTEN -t`.nothrow().quiet().text();
|
|
387
|
+
const pid = parseInt(result.trim());
|
|
388
|
+
if (!isNaN(pid) && pid > 0)
|
|
389
|
+
return pid;
|
|
390
|
+
}
|
|
391
|
+
} catch {}
|
|
392
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
214
396
|
async function readFileTail(filePath, lines) {
|
|
215
397
|
return await plat.measure(`Read tail ${lines ?? "all"}L`, async () => {
|
|
216
398
|
try {
|
|
@@ -227,6 +409,13 @@ async function readFileTail(filePath, lines) {
|
|
|
227
409
|
}
|
|
228
410
|
}) ?? "";
|
|
229
411
|
}
|
|
412
|
+
function copyFile(src, dest) {
|
|
413
|
+
fs.copyFileSync(src, dest);
|
|
414
|
+
}
|
|
415
|
+
async function getProcessMemory(pid) {
|
|
416
|
+
const map = await getProcessBatchResources([pid]);
|
|
417
|
+
return map.get(pid)?.memory || 0;
|
|
418
|
+
}
|
|
230
419
|
async function getProcessBatchResources(pids) {
|
|
231
420
|
if (pids.length === 0)
|
|
232
421
|
return new Map;
|
|
@@ -235,28 +424,16 @@ async function getProcessBatchResources(pids) {
|
|
|
235
424
|
const pidSet = new Set(pids);
|
|
236
425
|
try {
|
|
237
426
|
if (isWindows()) {
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
`)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (!trimmed || trimmed.startsWith("Id") || trimmed.startsWith("--"))
|
|
427
|
+
const output = psExec(`Get-Process -Id ${pids.join(",")} -ErrorAction SilentlyContinue | Select-Object Id, WorkingSet64 | ForEach-Object { Write-Output "$($_.Id)|$($_.WorkingSet64)" }`);
|
|
428
|
+
for (const line of output.split(`
|
|
429
|
+
`)) {
|
|
430
|
+
const sepIdx = line.indexOf("|");
|
|
431
|
+
if (sepIdx === -1)
|
|
244
432
|
continue;
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
let memStr = parts[2];
|
|
250
|
-
if (parts.length === 2) {
|
|
251
|
-
cpuStr = "0";
|
|
252
|
-
memStr = parts[1];
|
|
253
|
-
}
|
|
254
|
-
const cpu = parseFloat(cpuStr) || 0;
|
|
255
|
-
const memory = parseInt(memStr) || 0;
|
|
256
|
-
if (!isNaN(pid) && !isNaN(memory)) {
|
|
257
|
-
if (pidSet.has(pid))
|
|
258
|
-
resourceMap.set(pid, { memory, cpu });
|
|
259
|
-
}
|
|
433
|
+
const pid = parseInt(line.substring(0, sepIdx).trim());
|
|
434
|
+
const memory = parseInt(line.substring(sepIdx + 1).trim()) || 0;
|
|
435
|
+
if (!isNaN(pid) && pidSet.has(pid)) {
|
|
436
|
+
resourceMap.set(pid, { memory, cpu: 0 });
|
|
260
437
|
}
|
|
261
438
|
}
|
|
262
439
|
} else {
|
|
@@ -309,15 +486,13 @@ async function getProcessPorts(pid) {
|
|
|
309
486
|
if (ports2.size > 0)
|
|
310
487
|
return Array.from(ports2);
|
|
311
488
|
} catch {}
|
|
312
|
-
const result = await $`lsof -
|
|
489
|
+
const result = await $`lsof -Pan -p ${pid} -iTCP -sTCP:LISTEN`.nothrow().quiet().text();
|
|
313
490
|
const ports = new Set;
|
|
314
491
|
for (const line of result.split(`
|
|
315
492
|
`)) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
ports.add(parseInt(portMatch[1]));
|
|
320
|
-
}
|
|
493
|
+
const portMatch = line.match(/:(\d+)\s+\(LISTEN\)/);
|
|
494
|
+
if (portMatch) {
|
|
495
|
+
ports.add(parseInt(portMatch[1]));
|
|
321
496
|
}
|
|
322
497
|
}
|
|
323
498
|
return Array.from(ports);
|
|
@@ -333,7 +508,6 @@ var init_platform = __esm(() => {
|
|
|
333
508
|
|
|
334
509
|
// src/utils.ts
|
|
335
510
|
import * as fs2 from "fs";
|
|
336
|
-
import chalk from "chalk";
|
|
337
511
|
function parseEnvString(envString) {
|
|
338
512
|
const env = {};
|
|
339
513
|
envString.split(",").forEach((pair) => {
|
|
@@ -351,8 +525,8 @@ function calculateRuntime(startTime) {
|
|
|
351
525
|
}
|
|
352
526
|
async function getVersion() {
|
|
353
527
|
try {
|
|
354
|
-
const { join } = await import("path");
|
|
355
|
-
const pkgPath =
|
|
528
|
+
const { join: join2 } = await import("path");
|
|
529
|
+
const pkgPath = join2(import.meta.dir, "../package.json");
|
|
356
530
|
const pkg = await Bun.file(pkgPath).json();
|
|
357
531
|
return pkg.version || "0.0.0";
|
|
358
532
|
} catch {
|
|
@@ -361,8 +535,7 @@ async function getVersion() {
|
|
|
361
535
|
}
|
|
362
536
|
function validateDirectory(directory) {
|
|
363
537
|
if (!directory || !fs2.existsSync(directory)) {
|
|
364
|
-
|
|
365
|
-
process.exit(1);
|
|
538
|
+
throw new Error(`Directory not found or invalid: '${directory}'`);
|
|
366
539
|
}
|
|
367
540
|
}
|
|
368
541
|
function tailFile(path, prefix, colorFn, lines) {
|
|
@@ -417,21 +590,39 @@ var exports_db = {};
|
|
|
417
590
|
__export(exports_db, {
|
|
418
591
|
updateProcessPid: () => updateProcessPid,
|
|
419
592
|
updateProcessEnv: () => updateProcessEnv,
|
|
593
|
+
saveTemplate: () => saveTemplate,
|
|
420
594
|
retryDatabaseOperation: () => retryDatabaseOperation,
|
|
421
595
|
removeProcessByName: () => removeProcessByName,
|
|
422
596
|
removeProcess: () => removeProcess,
|
|
597
|
+
removeDependency: () => removeDependency,
|
|
423
598
|
removeAllProcesses: () => removeAllProcesses,
|
|
599
|
+
removeAllDependencies: () => removeAllDependencies,
|
|
424
600
|
insertProcess: () => insertProcess,
|
|
601
|
+
getTemplate: () => getTemplate,
|
|
602
|
+
getStartOrder: () => getStartOrder,
|
|
603
|
+
getRecentHistory: () => getRecentHistory,
|
|
604
|
+
getProcessHistory: () => getProcessHistory,
|
|
425
605
|
getProcess: () => getProcess,
|
|
606
|
+
getDependents: () => getDependents,
|
|
607
|
+
getDependencyGraph: () => getDependencyGraph,
|
|
608
|
+
getDependencies: () => getDependencies,
|
|
426
609
|
getDbInfo: () => getDbInfo,
|
|
610
|
+
getAllTemplates: () => getAllTemplates,
|
|
427
611
|
getAllProcesses: () => getAllProcesses,
|
|
612
|
+
deleteTemplate: () => deleteTemplate,
|
|
428
613
|
dbPath: () => dbPath,
|
|
429
614
|
db: () => db,
|
|
615
|
+
clearOldHistory: () => clearOldHistory,
|
|
430
616
|
bgrHome: () => bgrHome,
|
|
431
|
-
|
|
617
|
+
addHistoryEntry: () => addHistoryEntry,
|
|
618
|
+
addDependency: () => addDependency,
|
|
619
|
+
TemplateSchema: () => TemplateSchema,
|
|
620
|
+
ProcessSchema: () => ProcessSchema,
|
|
621
|
+
HistorySchema: () => HistorySchema,
|
|
622
|
+
DependencySchema: () => DependencySchema
|
|
432
623
|
});
|
|
433
624
|
import { Database, z } from "sqlite-zod-orm";
|
|
434
|
-
import { join } from "path";
|
|
625
|
+
import { join as join2 } from "path";
|
|
435
626
|
var {sleep } = globalThis.Bun;
|
|
436
627
|
import { existsSync as existsSync3, copyFileSync as copyFileSync2 } from "fs";
|
|
437
628
|
function getProcess(name) {
|
|
@@ -476,6 +667,154 @@ function updateProcessEnv(name, envJson) {
|
|
|
476
667
|
db.process.update(proc.id, { env: envJson });
|
|
477
668
|
}
|
|
478
669
|
}
|
|
670
|
+
function getAllTemplates() {
|
|
671
|
+
return db.template.select().all();
|
|
672
|
+
}
|
|
673
|
+
function getTemplate(name) {
|
|
674
|
+
return db.template.select().where({ name }).limit(1).get() || null;
|
|
675
|
+
}
|
|
676
|
+
function saveTemplate(data) {
|
|
677
|
+
const existing = db.template.select().where({ name: data.name }).limit(1).get();
|
|
678
|
+
if (existing) {
|
|
679
|
+
db.template.update(existing.id, {
|
|
680
|
+
command: data.command,
|
|
681
|
+
workdir: data.workdir || "",
|
|
682
|
+
env: data.env || "",
|
|
683
|
+
group: data.group || ""
|
|
684
|
+
});
|
|
685
|
+
} else {
|
|
686
|
+
db.template.insert({
|
|
687
|
+
name: data.name,
|
|
688
|
+
command: data.command,
|
|
689
|
+
workdir: data.workdir || "",
|
|
690
|
+
env: data.env || "",
|
|
691
|
+
group: data.group || ""
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
function deleteTemplate(name) {
|
|
696
|
+
const tmpl = db.template.select().where({ name }).limit(1).get();
|
|
697
|
+
if (tmpl) {
|
|
698
|
+
db.template.delete(tmpl.id);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
function getProcessHistory(name, limit = 50) {
|
|
702
|
+
return db.history.select().where({ process_name: name }).orderBy("timestamp", "desc").limit(limit).all();
|
|
703
|
+
}
|
|
704
|
+
function addHistoryEntry(processName, event, pid, metadata = {}) {
|
|
705
|
+
return db.history.insert({
|
|
706
|
+
process_name: processName,
|
|
707
|
+
event,
|
|
708
|
+
pid,
|
|
709
|
+
metadata: JSON.stringify(metadata)
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
function getRecentHistory(limit = 100) {
|
|
713
|
+
return db.history.select().orderBy("timestamp", "desc").limit(limit).all();
|
|
714
|
+
}
|
|
715
|
+
function clearOldHistory(daysToKeep = 30) {
|
|
716
|
+
const cutoff = new Date;
|
|
717
|
+
cutoff.setDate(cutoff.getDate() - daysToKeep);
|
|
718
|
+
const cutoffStr = cutoff.toISOString();
|
|
719
|
+
const oldEntries = db.history.select().where("timestamp", "<", cutoffStr).all();
|
|
720
|
+
for (const entry of oldEntries) {
|
|
721
|
+
db.history.delete(entry.id);
|
|
722
|
+
}
|
|
723
|
+
return oldEntries.length;
|
|
724
|
+
}
|
|
725
|
+
function getDependencies(processName) {
|
|
726
|
+
return db.dependency.select().where({ process_name: processName }).all().map((d) => d.depends_on);
|
|
727
|
+
}
|
|
728
|
+
function getDependents(processName) {
|
|
729
|
+
return db.dependency.select().where({ depends_on: processName }).all().map((d) => d.process_name);
|
|
730
|
+
}
|
|
731
|
+
function getDependencyGraph() {
|
|
732
|
+
const all = db.dependency.select().all();
|
|
733
|
+
const graph = {};
|
|
734
|
+
for (const dep of all) {
|
|
735
|
+
if (!graph[dep.process_name])
|
|
736
|
+
graph[dep.process_name] = [];
|
|
737
|
+
graph[dep.process_name].push(dep.depends_on);
|
|
738
|
+
}
|
|
739
|
+
return graph;
|
|
740
|
+
}
|
|
741
|
+
function addDependency(processName, dependsOn) {
|
|
742
|
+
if (processName === dependsOn)
|
|
743
|
+
return false;
|
|
744
|
+
const existing = db.dependency.select().where({ process_name: processName, depends_on: dependsOn }).limit(1).get();
|
|
745
|
+
if (existing)
|
|
746
|
+
return false;
|
|
747
|
+
if (wouldCreateCycle(processName, dependsOn))
|
|
748
|
+
return false;
|
|
749
|
+
db.dependency.insert({ process_name: processName, depends_on: dependsOn });
|
|
750
|
+
return true;
|
|
751
|
+
}
|
|
752
|
+
function removeDependency(processName, dependsOn) {
|
|
753
|
+
const matches = db.dependency.select().where({ process_name: processName, depends_on: dependsOn }).all();
|
|
754
|
+
for (const dep of matches) {
|
|
755
|
+
db.dependency.delete(dep.id);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
function removeAllDependencies(processName) {
|
|
759
|
+
const matches = db.dependency.select().where({ process_name: processName }).all();
|
|
760
|
+
for (const dep of matches) {
|
|
761
|
+
db.dependency.delete(dep.id);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
function wouldCreateCycle(processName, dependsOn) {
|
|
765
|
+
const graph = getDependencyGraph();
|
|
766
|
+
if (!graph[processName])
|
|
767
|
+
graph[processName] = [];
|
|
768
|
+
graph[processName].push(dependsOn);
|
|
769
|
+
const visited = new Set;
|
|
770
|
+
const stack = [dependsOn];
|
|
771
|
+
while (stack.length > 0) {
|
|
772
|
+
const current = stack.pop();
|
|
773
|
+
if (current === processName)
|
|
774
|
+
return true;
|
|
775
|
+
if (visited.has(current))
|
|
776
|
+
continue;
|
|
777
|
+
visited.add(current);
|
|
778
|
+
for (const dep of graph[current] || []) {
|
|
779
|
+
stack.push(dep);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
function getStartOrder() {
|
|
785
|
+
const graph = getDependencyGraph();
|
|
786
|
+
const allProcesses = getAllProcesses().map((p) => p.name);
|
|
787
|
+
const allNames = new Set(allProcesses);
|
|
788
|
+
const inDegree = {};
|
|
789
|
+
for (const name of allNames)
|
|
790
|
+
inDegree[name] = 0;
|
|
791
|
+
for (const [proc, deps] of Object.entries(graph)) {
|
|
792
|
+
for (const dep of deps) {
|
|
793
|
+
if (allNames.has(dep)) {
|
|
794
|
+
inDegree[proc] = (inDegree[proc] || 0) + 1;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
const queue = [];
|
|
799
|
+
for (const name of allNames) {
|
|
800
|
+
if ((inDegree[name] || 0) === 0)
|
|
801
|
+
queue.push(name);
|
|
802
|
+
}
|
|
803
|
+
const order = [];
|
|
804
|
+
while (queue.length > 0) {
|
|
805
|
+
queue.sort();
|
|
806
|
+
const current = queue.shift();
|
|
807
|
+
order.push(current);
|
|
808
|
+
for (const [proc, deps] of Object.entries(graph)) {
|
|
809
|
+
if (deps.includes(current) && allNames.has(proc)) {
|
|
810
|
+
inDegree[proc]--;
|
|
811
|
+
if (inDegree[proc] === 0)
|
|
812
|
+
queue.push(proc);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return order;
|
|
817
|
+
}
|
|
479
818
|
function getDbInfo() {
|
|
480
819
|
return {
|
|
481
820
|
dbPath,
|
|
@@ -498,7 +837,7 @@ async function retryDatabaseOperation(operation, maxRetries = 5, delay = 100) {
|
|
|
498
837
|
}
|
|
499
838
|
throw new Error("Max retries reached for database operation");
|
|
500
839
|
}
|
|
501
|
-
var ProcessSchema, homePath, bgrDir, dbFilename, dbPath, bgrHome, legacyDbPath, db;
|
|
840
|
+
var ProcessSchema, TemplateSchema, HistorySchema, DependencySchema, homePath, bgrDir, dbFilename, dbPath, bgrHome, legacyDbPath, db;
|
|
502
841
|
var init_db = __esm(() => {
|
|
503
842
|
init_platform();
|
|
504
843
|
ProcessSchema = z.object({
|
|
@@ -510,15 +849,36 @@ var init_db = __esm(() => {
|
|
|
510
849
|
configPath: z.string().default(""),
|
|
511
850
|
stdout_path: z.string(),
|
|
512
851
|
stderr_path: z.string(),
|
|
513
|
-
timestamp: z.string().default(() => new Date().toISOString())
|
|
852
|
+
timestamp: z.string().default(() => new Date().toISOString()),
|
|
853
|
+
group: z.string().default("")
|
|
854
|
+
});
|
|
855
|
+
TemplateSchema = z.object({
|
|
856
|
+
name: z.string(),
|
|
857
|
+
command: z.string(),
|
|
858
|
+
workdir: z.string().default(""),
|
|
859
|
+
env: z.string().default(""),
|
|
860
|
+
group: z.string().default(""),
|
|
861
|
+
created_at: z.string().default(() => new Date().toISOString())
|
|
862
|
+
});
|
|
863
|
+
HistorySchema = z.object({
|
|
864
|
+
process_name: z.string(),
|
|
865
|
+
event: z.string(),
|
|
866
|
+
pid: z.number().optional(),
|
|
867
|
+
timestamp: z.string().default(() => new Date().toISOString()),
|
|
868
|
+
metadata: z.string().default("")
|
|
869
|
+
});
|
|
870
|
+
DependencySchema = z.object({
|
|
871
|
+
process_name: z.string(),
|
|
872
|
+
depends_on: z.string(),
|
|
873
|
+
created_at: z.string().default(() => new Date().toISOString())
|
|
514
874
|
});
|
|
515
875
|
homePath = getHomeDir();
|
|
516
|
-
bgrDir =
|
|
876
|
+
bgrDir = join2(homePath, ".bgr");
|
|
517
877
|
ensureDir(bgrDir);
|
|
518
878
|
dbFilename = process.env.BGRUN_DB ?? "bgrun.sqlite";
|
|
519
|
-
dbPath =
|
|
879
|
+
dbPath = join2(bgrDir, dbFilename);
|
|
520
880
|
bgrHome = bgrDir;
|
|
521
|
-
legacyDbPath =
|
|
881
|
+
legacyDbPath = join2(bgrDir, "bgr_v2.sqlite");
|
|
522
882
|
if (!existsSync3(dbPath) && existsSync3(legacyDbPath)) {
|
|
523
883
|
try {
|
|
524
884
|
copyFileSync2(legacyDbPath, dbPath);
|
|
@@ -526,17 +886,23 @@ var init_db = __esm(() => {
|
|
|
526
886
|
} catch (e) {}
|
|
527
887
|
}
|
|
528
888
|
db = new Database(dbPath, {
|
|
529
|
-
process: ProcessSchema
|
|
889
|
+
process: ProcessSchema,
|
|
890
|
+
template: TemplateSchema,
|
|
891
|
+
history: HistorySchema,
|
|
892
|
+
dependency: DependencySchema
|
|
530
893
|
}, {
|
|
531
894
|
indexes: {
|
|
532
|
-
process: ["name", "timestamp", "pid"]
|
|
895
|
+
process: ["name", "timestamp", "pid"],
|
|
896
|
+
template: ["name"],
|
|
897
|
+
history: ["process_name", "timestamp"],
|
|
898
|
+
dependency: ["process_name", "depends_on"]
|
|
533
899
|
}
|
|
534
900
|
});
|
|
535
901
|
});
|
|
536
902
|
|
|
537
903
|
// src/logger.ts
|
|
538
904
|
import boxen from "boxen";
|
|
539
|
-
import
|
|
905
|
+
import chalk from "chalk";
|
|
540
906
|
function announce(message, title) {
|
|
541
907
|
console.log(boxen(message, {
|
|
542
908
|
padding: 1,
|
|
@@ -549,7 +915,7 @@ function announce(message, title) {
|
|
|
549
915
|
}
|
|
550
916
|
function error(message) {
|
|
551
917
|
const text = message instanceof Error ? message.stack || message.message : String(message);
|
|
552
|
-
console.error(boxen(
|
|
918
|
+
console.error(boxen(chalk.red(text), {
|
|
553
919
|
padding: 1,
|
|
554
920
|
margin: 1,
|
|
555
921
|
borderColor: "red",
|
|
@@ -557,9 +923,17 @@ function error(message) {
|
|
|
557
923
|
titleAlignment: "center",
|
|
558
924
|
borderStyle: "double"
|
|
559
925
|
}));
|
|
560
|
-
|
|
926
|
+
throw new BgrunError(text);
|
|
561
927
|
}
|
|
562
|
-
var
|
|
928
|
+
var BgrunError;
|
|
929
|
+
var init_logger = __esm(() => {
|
|
930
|
+
BgrunError = class BgrunError extends Error {
|
|
931
|
+
constructor(message) {
|
|
932
|
+
super(message);
|
|
933
|
+
this.name = "BgrunError";
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
});
|
|
563
937
|
|
|
564
938
|
// src/config.ts
|
|
565
939
|
function formatEnvKey(key) {
|
|
@@ -596,10 +970,10 @@ async function parseConfigFile(configPath) {
|
|
|
596
970
|
var exports_deps = {};
|
|
597
971
|
__export(exports_deps, {
|
|
598
972
|
getUnmetDeps: () => getUnmetDeps,
|
|
599
|
-
getDependencies: () =>
|
|
973
|
+
getDependencies: () => getDependencies2,
|
|
600
974
|
buildDepGraph: () => buildDepGraph
|
|
601
975
|
});
|
|
602
|
-
function
|
|
976
|
+
function getDependencies2(envStr) {
|
|
603
977
|
const env = parseEnvString(envStr);
|
|
604
978
|
const raw = env.BGR_DEPENDS_ON || "";
|
|
605
979
|
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -608,7 +982,7 @@ async function buildDepGraph() {
|
|
|
608
982
|
const processes = getAllProcesses();
|
|
609
983
|
const nodeMap = new Map;
|
|
610
984
|
for (const proc of processes) {
|
|
611
|
-
const deps =
|
|
985
|
+
const deps = getDependencies2(proc.env);
|
|
612
986
|
const alive = await isProcessRunning(proc.pid, proc.command);
|
|
613
987
|
nodeMap.set(proc.name, {
|
|
614
988
|
name: proc.name,
|
|
@@ -660,7 +1034,7 @@ async function getUnmetDeps(name) {
|
|
|
660
1034
|
const proc = getProcess(name);
|
|
661
1035
|
if (!proc)
|
|
662
1036
|
return [];
|
|
663
|
-
const deps =
|
|
1037
|
+
const deps = getDependencies2(proc.env);
|
|
664
1038
|
const unmet = [];
|
|
665
1039
|
for (const depName of deps) {
|
|
666
1040
|
const depProc = getProcess(depName);
|
|
@@ -684,10 +1058,10 @@ var init_deps = __esm(() => {
|
|
|
684
1058
|
// src/commands/run.ts
|
|
685
1059
|
var {$: $2 } = globalThis.Bun;
|
|
686
1060
|
var {sleep: sleep2 } = globalThis.Bun;
|
|
687
|
-
import { join as
|
|
1061
|
+
import { join as join3 } from "path";
|
|
688
1062
|
import { createMeasure as createMeasure2 } from "measure-fn";
|
|
689
1063
|
async function handleRun(options) {
|
|
690
|
-
const { command, directory, env, name, configPath, force, fetch, stdout, stderr } = options;
|
|
1064
|
+
const { command, directory, env, name, configPath, force, fetch: fetch2, stdout, stderr } = options;
|
|
691
1065
|
const existingProcess = name ? getProcess(name) : null;
|
|
692
1066
|
if (name && existingProcess) {
|
|
693
1067
|
const { getUnmetDeps: getUnmetDeps2 } = await Promise.resolve().then(() => (init_deps(), exports_deps));
|
|
@@ -708,7 +1082,7 @@ async function handleRun(options) {
|
|
|
708
1082
|
const finalDirectory2 = directory || existingProcess.workdir;
|
|
709
1083
|
validateDirectory(finalDirectory2);
|
|
710
1084
|
$2.cwd(finalDirectory2);
|
|
711
|
-
if (
|
|
1085
|
+
if (fetch2) {
|
|
712
1086
|
if (!__require("fs").existsSync(__require("path").join(finalDirectory2, ".git"))) {
|
|
713
1087
|
error(`Cannot --fetch: '${finalDirectory2}' is not a Git repository.`);
|
|
714
1088
|
}
|
|
@@ -758,9 +1132,15 @@ async function handleRun(options) {
|
|
|
758
1132
|
if (cmdToMatch) {
|
|
759
1133
|
await run.measure("Zombie sweep", async () => {
|
|
760
1134
|
try {
|
|
761
|
-
const
|
|
1135
|
+
const cmdKeyword = cmdToMatch.split(" ")[1] || cmdToMatch;
|
|
1136
|
+
const GENERIC_KEYWORDS = ["dev", "run", "start", "serve", "build", "test"];
|
|
1137
|
+
if (GENERIC_KEYWORDS.includes(cmdKeyword.toLowerCase())) {
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
const currentPid = process.pid;
|
|
1141
|
+
const result = await psExec(`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | Where-Object { $_.CommandLine -match '${cmdKeyword.replace(/'/g, "''")}' -and $_.ProcessId -ne ${currentPid} } | Select-Object -ExpandProperty ProcessId`, 3000);
|
|
762
1142
|
const zombiePids = result.split(`
|
|
763
|
-
`).map((l) => parseInt(l.trim())).filter((n) => !isNaN(n) && n > 0);
|
|
1143
|
+
`).map((l) => parseInt(l.trim())).filter((n) => !isNaN(n) && n > 0 && n !== currentPid);
|
|
764
1144
|
for (const zPid of zombiePids) {
|
|
765
1145
|
await $2`taskkill /F /T /PID ${zPid}`.nothrow().quiet();
|
|
766
1146
|
}
|
|
@@ -781,6 +1161,9 @@ async function handleRun(options) {
|
|
|
781
1161
|
const finalCommand = command || existingProcess.command;
|
|
782
1162
|
const finalDirectory = directory || existingProcess?.workdir;
|
|
783
1163
|
let finalEnv = env || (existingProcess ? parseEnvString(existingProcess.env) : {});
|
|
1164
|
+
if (!("BGR_KEEP_ALIVE" in finalEnv)) {
|
|
1165
|
+
finalEnv.BGR_KEEP_ALIVE = "true";
|
|
1166
|
+
}
|
|
784
1167
|
let finalConfigPath;
|
|
785
1168
|
if (configPath !== undefined) {
|
|
786
1169
|
finalConfigPath = configPath;
|
|
@@ -790,7 +1173,7 @@ async function handleRun(options) {
|
|
|
790
1173
|
finalConfigPath = ".config.toml";
|
|
791
1174
|
}
|
|
792
1175
|
if (finalConfigPath) {
|
|
793
|
-
const fullConfigPath =
|
|
1176
|
+
const fullConfigPath = join3(finalDirectory, finalConfigPath);
|
|
794
1177
|
if (await Bun.file(fullConfigPath).exists()) {
|
|
795
1178
|
const configEnv = await run.measure(`Parse config "${finalConfigPath}"`, async () => {
|
|
796
1179
|
try {
|
|
@@ -808,9 +1191,9 @@ async function handleRun(options) {
|
|
|
808
1191
|
console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
|
|
809
1192
|
}
|
|
810
1193
|
}
|
|
811
|
-
const stdoutPath = stdout || existingProcess?.stdout_path ||
|
|
1194
|
+
const stdoutPath = stdout || existingProcess?.stdout_path || join3(homePath2, ".bgr", `${name}-out.txt`);
|
|
812
1195
|
Bun.write(stdoutPath, "");
|
|
813
|
-
const stderrPath = stderr || existingProcess?.stderr_path ||
|
|
1196
|
+
const stderrPath = stderr || existingProcess?.stderr_path || join3(homePath2, ".bgr", `${name}-err.txt`);
|
|
814
1197
|
Bun.write(stderrPath, "");
|
|
815
1198
|
const actualPid = await run.measure(`Spawn "${name}" \u2192 ${finalCommand}`, async () => {
|
|
816
1199
|
const newProcess = Bun.spawn(getShellCommand(finalCommand), {
|
|
@@ -854,7 +1237,7 @@ __export(exports_log_rotation, {
|
|
|
854
1237
|
rotateLogFile: () => rotateLogFile,
|
|
855
1238
|
rotateAllLogs: () => rotateAllLogs
|
|
856
1239
|
});
|
|
857
|
-
import { existsSync as existsSync7, statSync as statSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
1240
|
+
import { existsSync as existsSync7, statSync as statSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
858
1241
|
function rotateLogFile(filePath, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
|
|
859
1242
|
try {
|
|
860
1243
|
if (!existsSync7(filePath))
|
|
@@ -870,7 +1253,7 @@ function rotateLogFile(filePath, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAU
|
|
|
870
1253
|
const truncated = lines.slice(-keepLines);
|
|
871
1254
|
const header = `--- [bgrun] Log rotated at ${new Date().toISOString()} (was ${lines.length} lines, ${formatBytes(stat.size)}) ---
|
|
872
1255
|
`;
|
|
873
|
-
|
|
1256
|
+
writeFileSync2(filePath, header + truncated.join(`
|
|
874
1257
|
`));
|
|
875
1258
|
return true;
|
|
876
1259
|
} catch {
|
|
@@ -922,22 +1305,87 @@ var init_log_rotation = __esm(() => {
|
|
|
922
1305
|
var exports_server = {};
|
|
923
1306
|
__export(exports_server, {
|
|
924
1307
|
startServer: () => startServer,
|
|
925
|
-
guardRestartCounts: () => guardRestartCounts
|
|
1308
|
+
guardRestartCounts: () => guardRestartCounts,
|
|
1309
|
+
guardEvents: () => guardEvents
|
|
926
1310
|
});
|
|
927
1311
|
import path2 from "path";
|
|
1312
|
+
async function cleanupPort(port) {
|
|
1313
|
+
if (process.platform !== "win32")
|
|
1314
|
+
return port;
|
|
1315
|
+
try {
|
|
1316
|
+
const proc = Bun.spawn([
|
|
1317
|
+
"powershell",
|
|
1318
|
+
"-NoProfile",
|
|
1319
|
+
"-Command",
|
|
1320
|
+
`Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
|
|
1321
|
+
], { stdout: "pipe", stderr: "pipe" });
|
|
1322
|
+
const text = await new Response(proc.stdout).text();
|
|
1323
|
+
const pid = parseInt(text.trim(), 10);
|
|
1324
|
+
if (!pid || pid === process.pid)
|
|
1325
|
+
return port;
|
|
1326
|
+
const checkProc = Bun.spawn([
|
|
1327
|
+
"powershell",
|
|
1328
|
+
"-NoProfile",
|
|
1329
|
+
"-Command",
|
|
1330
|
+
`Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`
|
|
1331
|
+
], { stdout: "pipe", stderr: "pipe" });
|
|
1332
|
+
const checkText = await new Response(checkProc.stdout).text();
|
|
1333
|
+
if (checkText.trim()) {
|
|
1334
|
+
console.log(`[server] Killing PID ${pid} holding port ${port}`);
|
|
1335
|
+
Bun.spawn(["taskkill", "/F", "/PID", String(pid)], { stdout: "pipe", stderr: "pipe" });
|
|
1336
|
+
await Bun.sleep(1000);
|
|
1337
|
+
return port;
|
|
1338
|
+
} else {
|
|
1339
|
+
const fallback = port + 1;
|
|
1340
|
+
console.log(`[server] \u26A0 Port ${port} held by zombie PID ${pid} \u2014 falling back to port ${fallback}`);
|
|
1341
|
+
return fallback;
|
|
1342
|
+
}
|
|
1343
|
+
} catch {
|
|
1344
|
+
return port;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
928
1347
|
async function startServer() {
|
|
929
1348
|
const { start } = await import("melina");
|
|
930
1349
|
const appDir = path2.join(import.meta.dir, "../dashboard/app");
|
|
931
|
-
const
|
|
1350
|
+
const requestedPort = process.env.BUN_PORT ? parseInt(process.env.BUN_PORT, 10) : 3000;
|
|
1351
|
+
_originalPort = requestedPort;
|
|
1352
|
+
const resolvedPort = await cleanupPort(requestedPort);
|
|
1353
|
+
_currentPort = resolvedPort;
|
|
1354
|
+
const needsExplicitPort = process.env.BUN_PORT || resolvedPort !== requestedPort;
|
|
932
1355
|
await start({
|
|
933
1356
|
appDir,
|
|
934
1357
|
defaultTitle: "bgrun Dashboard - Process Manager",
|
|
935
1358
|
globalCss: path2.join(appDir, "globals.css"),
|
|
936
|
-
...
|
|
1359
|
+
...needsExplicitPort && { port: resolvedPort }
|
|
937
1360
|
});
|
|
938
1361
|
startGuard();
|
|
939
1362
|
const { startLogRotation: startLogRotation2 } = await Promise.resolve().then(() => (init_log_rotation(), exports_log_rotation));
|
|
940
1363
|
startLogRotation2(() => getAllProcesses());
|
|
1364
|
+
if (resolvedPort !== requestedPort) {
|
|
1365
|
+
startStickyPortChecker();
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
function startStickyPortChecker() {
|
|
1369
|
+
const CHECK_INTERVAL_MS = 60000;
|
|
1370
|
+
console.log(`[server] Starting sticky port checker (original: ${_originalPort}, current: ${_currentPort})`);
|
|
1371
|
+
setInterval(async () => {
|
|
1372
|
+
if (_currentPort === _originalPort)
|
|
1373
|
+
return;
|
|
1374
|
+
try {
|
|
1375
|
+
const proc = Bun.spawn([
|
|
1376
|
+
"powershell",
|
|
1377
|
+
"-NoProfile",
|
|
1378
|
+
"-Command",
|
|
1379
|
+
`Get-NetTCPConnection -LocalPort ${_originalPort} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
|
|
1380
|
+
], { stdout: "pipe", stderr: "pipe" });
|
|
1381
|
+
const text = await new Response(proc.stdout).text();
|
|
1382
|
+
const pid = parseInt(text.trim(), 10);
|
|
1383
|
+
if (!pid) {
|
|
1384
|
+
console.log(`[server] \u2713 Original port ${_originalPort} is now available! Consider restarting to reclaim it.`);
|
|
1385
|
+
_currentPort = _originalPort;
|
|
1386
|
+
}
|
|
1387
|
+
} catch {}
|
|
1388
|
+
}, CHECK_INTERVAL_MS);
|
|
941
1389
|
}
|
|
942
1390
|
function startGuard() {
|
|
943
1391
|
console.log(`[guard] \u2713 Built-in process guard started (checking every ${GUARD_INTERVAL_MS / 1000}s)`);
|
|
@@ -959,6 +1407,7 @@ function startGuard() {
|
|
|
959
1407
|
if (now < nextRestart)
|
|
960
1408
|
continue;
|
|
961
1409
|
console.log(`[guard] \u26A0 Guarded process "${proc.name}" (PID ${proc.pid}) is dead, restarting...`);
|
|
1410
|
+
let success = false;
|
|
962
1411
|
try {
|
|
963
1412
|
await handleRun({
|
|
964
1413
|
action: "run",
|
|
@@ -966,9 +1415,16 @@ function startGuard() {
|
|
|
966
1415
|
force: true,
|
|
967
1416
|
remoteName: ""
|
|
968
1417
|
});
|
|
1418
|
+
success = true;
|
|
969
1419
|
const prevCount = guardRestartCounts.get(proc.name) || 0;
|
|
970
1420
|
const newCount = prevCount + 1;
|
|
971
1421
|
guardRestartCounts.set(proc.name, newCount);
|
|
1422
|
+
try {
|
|
1423
|
+
addHistoryEntry(proc.name, "restart", undefined, { by: "guard", count: newCount });
|
|
1424
|
+
} catch {}
|
|
1425
|
+
guardEvents.unshift({ time: now, name: proc.name, action: "restart", success: true });
|
|
1426
|
+
if (guardEvents.length > 100)
|
|
1427
|
+
guardEvents.pop();
|
|
972
1428
|
if (newCount > 5) {
|
|
973
1429
|
const backoffSeconds = Math.min(30 * Math.pow(2, newCount - 6), 300);
|
|
974
1430
|
guardNextRestartTime.set(proc.name, Date.now() + backoffSeconds * 1000);
|
|
@@ -978,6 +1434,9 @@ function startGuard() {
|
|
|
978
1434
|
}
|
|
979
1435
|
} catch (err) {
|
|
980
1436
|
console.error(`[guard] \u2717 Failed to restart "${proc.name}": ${err.message}`);
|
|
1437
|
+
guardEvents.unshift({ time: now, name: proc.name, action: "restart", success: false });
|
|
1438
|
+
if (guardEvents.length > 100)
|
|
1439
|
+
guardEvents.pop();
|
|
981
1440
|
}
|
|
982
1441
|
} else {
|
|
983
1442
|
const prevCount = guardRestartCounts.get(proc.name) || 0;
|
|
@@ -995,7 +1454,7 @@ function startGuard() {
|
|
|
995
1454
|
}
|
|
996
1455
|
}, GUARD_INTERVAL_MS);
|
|
997
1456
|
}
|
|
998
|
-
var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES, _g, guardRestartCounts, guardNextRestartTime;
|
|
1457
|
+
var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES, _g, guardRestartCounts, guardNextRestartTime, guardEvents, _originalPort = 3000, _currentPort = 3000;
|
|
999
1458
|
var init_server = __esm(() => {
|
|
1000
1459
|
init_db();
|
|
1001
1460
|
init_platform();
|
|
@@ -1007,8 +1466,11 @@ var init_server = __esm(() => {
|
|
|
1007
1466
|
_g.__bgrGuardRestartCounts = new Map;
|
|
1008
1467
|
if (!_g.__bgrGuardNextRestartTime)
|
|
1009
1468
|
_g.__bgrGuardNextRestartTime = new Map;
|
|
1469
|
+
if (!_g.__bgrGuardEvents)
|
|
1470
|
+
_g.__bgrGuardEvents = [];
|
|
1010
1471
|
guardRestartCounts = _g.__bgrGuardRestartCounts;
|
|
1011
1472
|
guardNextRestartTime = _g.__bgrGuardNextRestartTime;
|
|
1473
|
+
guardEvents = _g.__bgrGuardEvents;
|
|
1012
1474
|
});
|
|
1013
1475
|
|
|
1014
1476
|
// src/guard.ts
|
|
@@ -1016,6 +1478,38 @@ var exports_guard = {};
|
|
|
1016
1478
|
__export(exports_guard, {
|
|
1017
1479
|
startGuardLoop: () => startGuardLoop
|
|
1018
1480
|
});
|
|
1481
|
+
import { createHmac } from "crypto";
|
|
1482
|
+
async function notifyWebhook(event, name, details) {
|
|
1483
|
+
if (!WEBHOOK_URL)
|
|
1484
|
+
return;
|
|
1485
|
+
try {
|
|
1486
|
+
const payload = JSON.stringify({
|
|
1487
|
+
event,
|
|
1488
|
+
process: name,
|
|
1489
|
+
timestamp: new Date().toISOString(),
|
|
1490
|
+
...details
|
|
1491
|
+
});
|
|
1492
|
+
const headers = {
|
|
1493
|
+
"Content-Type": "application/json",
|
|
1494
|
+
"User-Agent": "bgrun-guard/1.0"
|
|
1495
|
+
};
|
|
1496
|
+
if (WEBHOOK_SECRET) {
|
|
1497
|
+
const sig = createHmac("sha256", WEBHOOK_SECRET).update(payload).digest("hex");
|
|
1498
|
+
headers["X-BGR-Signature"] = `sha256=${sig}`;
|
|
1499
|
+
}
|
|
1500
|
+
const controller = new AbortController;
|
|
1501
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
1502
|
+
await fetch(WEBHOOK_URL, {
|
|
1503
|
+
method: "POST",
|
|
1504
|
+
headers,
|
|
1505
|
+
body: payload,
|
|
1506
|
+
signal: controller.signal
|
|
1507
|
+
});
|
|
1508
|
+
clearTimeout(timeout);
|
|
1509
|
+
} catch (err) {
|
|
1510
|
+
console.error(`[guard] Webhook failed: ${err.message}`);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1019
1513
|
async function restartProcess(name) {
|
|
1020
1514
|
try {
|
|
1021
1515
|
await handleRun({
|
|
@@ -1064,6 +1558,7 @@ async function guardCycle() {
|
|
|
1064
1558
|
continue;
|
|
1065
1559
|
}
|
|
1066
1560
|
console.log(`[guard] \u26A0 "${proc.name}" (PID ${proc.pid}) is dead \u2014 restarting...`);
|
|
1561
|
+
notifyWebhook("crash", proc.name, { pid: proc.pid, isDashboard });
|
|
1067
1562
|
const success = await restartProcess(proc.name);
|
|
1068
1563
|
if (success) {
|
|
1069
1564
|
const count = (state.restartCounts.get(proc.name) || 0) + 1;
|
|
@@ -1077,6 +1572,9 @@ async function guardCycle() {
|
|
|
1077
1572
|
console.log(`[guard] \u2713 Restarted "${proc.name}" (#${count})`);
|
|
1078
1573
|
}
|
|
1079
1574
|
restarted++;
|
|
1575
|
+
notifyWebhook("restart", proc.name, { pid: proc.pid, restartCount: count, backoffMs: backoff });
|
|
1576
|
+
} else {
|
|
1577
|
+
notifyWebhook("restart_failed", proc.name, { pid: proc.pid });
|
|
1080
1578
|
}
|
|
1081
1579
|
} else if (alive) {
|
|
1082
1580
|
const count = state.restartCounts.get(proc.name) || 0;
|
|
@@ -1111,17 +1609,20 @@ async function startGuardLoop(intervalMs = DEFAULT_INTERVAL_MS) {
|
|
|
1111
1609
|
console.log(`[guard] Crash backoff threshold: ${CRASH_THRESHOLD} restarts`);
|
|
1112
1610
|
console.log(`[guard] Stability window: ${STABILITY_WINDOW_MS / 1000}s`);
|
|
1113
1611
|
console.log(`[guard] Monitoring: BGR_KEEP_ALIVE=true + bgr-dashboard`);
|
|
1612
|
+
console.log(`[guard] Webhook: ${WEBHOOK_URL || "(none \u2014 set BGR_WEBHOOK_URL to enable)"}`);
|
|
1114
1613
|
console.log(`[guard] Started: ${new Date().toLocaleString()}`);
|
|
1115
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`);
|
|
1116
1615
|
await guardCycle();
|
|
1117
1616
|
setInterval(guardCycle, interval);
|
|
1118
1617
|
}
|
|
1119
|
-
var DEFAULT_INTERVAL_MS = 30000, MAX_BACKOFF_MS, CRASH_THRESHOLD = 5, STABILITY_WINDOW_MS = 120000, state;
|
|
1618
|
+
var WEBHOOK_URL, WEBHOOK_SECRET, DEFAULT_INTERVAL_MS = 30000, MAX_BACKOFF_MS, CRASH_THRESHOLD = 5, STABILITY_WINDOW_MS = 120000, state;
|
|
1120
1619
|
var init_guard = __esm(() => {
|
|
1121
1620
|
init_db();
|
|
1122
1621
|
init_platform();
|
|
1123
1622
|
init_run();
|
|
1124
1623
|
init_utils();
|
|
1624
|
+
WEBHOOK_URL = process.env.BGR_WEBHOOK_URL || "";
|
|
1625
|
+
WEBHOOK_SECRET = process.env.BGR_WEBHOOK_SECRET || "";
|
|
1125
1626
|
MAX_BACKOFF_MS = 5 * 60000;
|
|
1126
1627
|
state = {
|
|
1127
1628
|
restartCounts: new Map,
|
|
@@ -1136,10 +1637,10 @@ init_run();
|
|
|
1136
1637
|
import { parseArgs } from "util";
|
|
1137
1638
|
|
|
1138
1639
|
// src/commands/list.ts
|
|
1139
|
-
import
|
|
1640
|
+
import chalk3 from "chalk";
|
|
1140
1641
|
|
|
1141
1642
|
// src/table.ts
|
|
1142
|
-
import
|
|
1643
|
+
import chalk2 from "chalk";
|
|
1143
1644
|
function getTerminalWidth() {
|
|
1144
1645
|
return process.stdout.columns || 120;
|
|
1145
1646
|
}
|
|
@@ -1221,7 +1722,7 @@ function renderBorder(widths, padding, style) {
|
|
|
1221
1722
|
function renderHorizontalTable(rows, columns, options = {}) {
|
|
1222
1723
|
const { maxWidth = getTerminalWidth(), padding = 2, borderStyle = "rounded", showHeaders = true } = options;
|
|
1223
1724
|
if (rows.length === 0)
|
|
1224
|
-
return { table:
|
|
1725
|
+
return { table: chalk2.gray("No data to display"), truncatedIndices: [] };
|
|
1225
1726
|
const borderChars = {
|
|
1226
1727
|
rounded: ["\u256D", "\u252C", "\u256E", "\u2500", "\u2502", "\u251C", "\u253C", "\u2524", "\u2570", "\u2534", "\u256F"],
|
|
1227
1728
|
single: ["\u250C", "\u252C", "\u2510", "\u2500", "\u2502", "\u251C", "\u253C", "\u2524", "\u2514", "\u2534", "\u2518"],
|
|
@@ -1237,7 +1738,7 @@ function renderHorizontalTable(rows, columns, options = {}) {
|
|
|
1237
1738
|
if (borderStyle !== "none")
|
|
1238
1739
|
lines.push(renderBorder(widthArray, padding, [tl, tc, tr, h]));
|
|
1239
1740
|
if (showHeaders) {
|
|
1240
|
-
const headerCells = columns.map((col, i) =>
|
|
1741
|
+
const headerCells = columns.map((col, i) => chalk2.bold(truncateString(col.header, widthArray[i]).padEnd(widthArray[i])));
|
|
1241
1742
|
lines.push(`${v}${cellPadding}${headerCells.join(`${cellPadding}${v}${cellPadding}`)}${cellPadding}${v}`);
|
|
1242
1743
|
if (borderStyle !== "none")
|
|
1243
1744
|
lines.push(renderBorder(widthArray, padding, [ml, mc, mr, h]));
|
|
@@ -1266,10 +1767,10 @@ function renderVerticalTree(rows, columns) {
|
|
|
1266
1767
|
if (index > 0)
|
|
1267
1768
|
lines.push("");
|
|
1268
1769
|
const name = row.name ? `'${row.name}'` : `(ID: ${row.id})`;
|
|
1269
|
-
lines.push(
|
|
1770
|
+
lines.push(chalk2.cyan(`\u25B6 ${name}`));
|
|
1270
1771
|
columns.forEach((col) => {
|
|
1271
1772
|
const value = col.formatter ? col.formatter(row[col.key]) : String(row[col.key] || "");
|
|
1272
|
-
lines.push(` \u251C\u2500 ${
|
|
1773
|
+
lines.push(` \u251C\u2500 ${chalk2.gray(`${col.header}:`)} ${value}`);
|
|
1273
1774
|
});
|
|
1274
1775
|
});
|
|
1275
1776
|
return lines.join(`
|
|
@@ -1288,15 +1789,15 @@ function renderHybridTable(rows, columns, options = {}) {
|
|
|
1288
1789
|
}
|
|
1289
1790
|
function renderProcessTable(processes, options) {
|
|
1290
1791
|
const columns = [
|
|
1291
|
-
{ key: "id", header: "ID", formatter: (id) =>
|
|
1292
|
-
{ key: "pid", header: "PID", formatter: (pid) =>
|
|
1293
|
-
{ key: "name", header: "Name", formatter: (name) =>
|
|
1294
|
-
{ key: "port", header: "Port", formatter: (port) => port === "-" ?
|
|
1295
|
-
{ key: "memory", header: "Memory", formatter: (mem) => mem === "-" ?
|
|
1792
|
+
{ key: "id", header: "ID", formatter: (id) => chalk2.blue(id) },
|
|
1793
|
+
{ key: "pid", header: "PID", formatter: (pid) => chalk2.yellow(pid) },
|
|
1794
|
+
{ key: "name", header: "Name", formatter: (name) => chalk2.cyan.bold(name) },
|
|
1795
|
+
{ key: "port", header: "Port", formatter: (port) => port === "-" ? chalk2.gray(port) : chalk2.hex("#FF6B6B")(port) },
|
|
1796
|
+
{ key: "memory", header: "Memory", formatter: (mem) => mem === "-" ? chalk2.gray(mem) : chalk2.hex("#4ECDC4")(mem) },
|
|
1296
1797
|
{ key: "command", header: "Command" },
|
|
1297
|
-
{ key: "workdir", header: "Directory", formatter: (dir) =>
|
|
1798
|
+
{ key: "workdir", header: "Directory", formatter: (dir) => chalk2.gray(dir), truncator: truncatePath },
|
|
1298
1799
|
{ key: "status", header: "Status" },
|
|
1299
|
-
{ key: "runtime", header: "Runtime", formatter: (runtime) =>
|
|
1800
|
+
{ key: "runtime", header: "Runtime", formatter: (runtime) => chalk2.magenta(runtime) }
|
|
1300
1801
|
];
|
|
1301
1802
|
return renderHybridTable(processes, columns, options);
|
|
1302
1803
|
}
|
|
@@ -1322,10 +1823,29 @@ async function showAll(opts) {
|
|
|
1322
1823
|
const envVars = parseEnvString(proc.env);
|
|
1323
1824
|
return envVars["BGR_GROUP"] === opts.filter;
|
|
1324
1825
|
});
|
|
1826
|
+
const deadPids = new Set;
|
|
1827
|
+
const aliveCache = new Map;
|
|
1828
|
+
for (const proc of filtered) {
|
|
1829
|
+
const alive = await isProcessRunning(proc.pid, proc.command);
|
|
1830
|
+
aliveCache.set(proc.pid, alive);
|
|
1831
|
+
if (!alive && proc.pid > 0)
|
|
1832
|
+
deadPids.add(proc.pid);
|
|
1833
|
+
}
|
|
1834
|
+
if (deadPids.size > 0) {
|
|
1835
|
+
const reconciled = await reconcileProcessPids(filtered.map((p) => ({ name: p.name, pid: p.pid, command: p.command, workdir: p.workdir })), deadPids);
|
|
1836
|
+
for (const [name, newPid] of reconciled) {
|
|
1837
|
+
updateProcessPid(name, newPid);
|
|
1838
|
+
const proc = filtered.find((p) => p.name === name);
|
|
1839
|
+
if (proc) {
|
|
1840
|
+
proc.pid = newPid;
|
|
1841
|
+
aliveCache.set(newPid, true);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1325
1845
|
if (opts?.json) {
|
|
1326
1846
|
const jsonData = [];
|
|
1327
1847
|
for (const proc of filtered) {
|
|
1328
|
-
const isRunning = await isProcessRunning(proc.pid, proc.command);
|
|
1848
|
+
const isRunning = aliveCache.get(proc.pid) ?? await isProcessRunning(proc.pid, proc.command);
|
|
1329
1849
|
const envVars = parseEnvString(proc.env);
|
|
1330
1850
|
const ports = isRunning ? await getProcessPorts(proc.pid) : [];
|
|
1331
1851
|
jsonData.push({
|
|
@@ -1343,7 +1863,7 @@ async function showAll(opts) {
|
|
|
1343
1863
|
const allPids = filtered.map((p) => p.pid);
|
|
1344
1864
|
const resourceMap = await getProcessBatchResources(allPids);
|
|
1345
1865
|
for (const proc of filtered) {
|
|
1346
|
-
const isRunning = await isProcessRunning(proc.pid, proc.command);
|
|
1866
|
+
const isRunning = aliveCache.get(proc.pid) ?? await isProcessRunning(proc.pid, proc.command);
|
|
1347
1867
|
const runtime = calculateRuntime(proc.timestamp);
|
|
1348
1868
|
const mem = isRunning ? resourceMap.get(proc.pid)?.memory || 0 : 0;
|
|
1349
1869
|
const ports = isRunning ? await getProcessPorts(proc.pid) : [];
|
|
@@ -1355,7 +1875,7 @@ async function showAll(opts) {
|
|
|
1355
1875
|
memory: formatMemory(mem),
|
|
1356
1876
|
command: proc.command,
|
|
1357
1877
|
workdir: proc.workdir,
|
|
1358
|
-
status: isRunning ?
|
|
1878
|
+
status: isRunning ? chalk3.green.bold("\u25CF Running") : chalk3.red.bold("\u25CB Stopped"),
|
|
1359
1879
|
runtime
|
|
1360
1880
|
});
|
|
1361
1881
|
}
|
|
@@ -1375,7 +1895,7 @@ async function showAll(opts) {
|
|
|
1375
1895
|
console.log(tableOutput);
|
|
1376
1896
|
const runningCount = tableData.filter((p) => p.status.includes("Running")).length;
|
|
1377
1897
|
const stoppedCount = tableData.filter((p) => p.status.includes("Stopped")).length;
|
|
1378
|
-
console.log(
|
|
1898
|
+
console.log(chalk3.cyan(`Total: ${tableData.length} processes (${chalk3.green(`${runningCount} running`)}, ${chalk3.red(`${stoppedCount} stopped`)})`));
|
|
1379
1899
|
}
|
|
1380
1900
|
|
|
1381
1901
|
// src/commands/cleanup.ts
|
|
@@ -1506,7 +2026,7 @@ init_utils();
|
|
|
1506
2026
|
init_run();
|
|
1507
2027
|
import * as fs4 from "fs";
|
|
1508
2028
|
import path from "path";
|
|
1509
|
-
import
|
|
2029
|
+
import chalk4 from "chalk";
|
|
1510
2030
|
async function handleWatch(options, logOptions) {
|
|
1511
2031
|
let currentProcess = null;
|
|
1512
2032
|
let isRestarting = false;
|
|
@@ -1517,7 +2037,7 @@ async function handleWatch(options, logOptions) {
|
|
|
1517
2037
|
const isDead = !await isProcessRunning(proc.pid);
|
|
1518
2038
|
if (!isDead)
|
|
1519
2039
|
return false;
|
|
1520
|
-
console.log(
|
|
2040
|
+
console.log(chalk4.yellow(`\uD83D\uDC80 Process '${options.name}' died immediately after ${reason}\u2014dumping logs:`));
|
|
1521
2041
|
const readAndDump = (path2, color, label) => {
|
|
1522
2042
|
try {
|
|
1523
2043
|
if (fs4.existsSync(path2)) {
|
|
@@ -1531,14 +2051,14 @@ ${color(content)}
|
|
|
1531
2051
|
}
|
|
1532
2052
|
}
|
|
1533
2053
|
} catch (err) {
|
|
1534
|
-
console.warn(
|
|
2054
|
+
console.warn(chalk4.gray(`Could not read ${label} log: ${err}`));
|
|
1535
2055
|
}
|
|
1536
2056
|
};
|
|
1537
2057
|
if (logOptions.logType === "both" || logOptions.logType === "stdout") {
|
|
1538
|
-
readAndDump(proc.stdout_path,
|
|
2058
|
+
readAndDump(proc.stdout_path, chalk4.white, "\uD83D\uDCC4 Stdout");
|
|
1539
2059
|
}
|
|
1540
2060
|
if (logOptions.logType === "both" || logOptions.logType === "stderr") {
|
|
1541
|
-
readAndDump(proc.stderr_path,
|
|
2061
|
+
readAndDump(proc.stderr_path, chalk4.red, "\uD83D\uDCC4 Stderr");
|
|
1542
2062
|
}
|
|
1543
2063
|
return true;
|
|
1544
2064
|
};
|
|
@@ -1579,29 +2099,29 @@ ${color(content)}
|
|
|
1579
2099
|
const stops = [];
|
|
1580
2100
|
if (!logOptions.showLogs || !currentProcess)
|
|
1581
2101
|
return stops;
|
|
1582
|
-
console.log(
|
|
2102
|
+
console.log(chalk4.gray(`
|
|
1583
2103
|
` + "\u2500".repeat(50) + `
|
|
1584
2104
|
`));
|
|
1585
2105
|
if (logOptions.logType === "both" || logOptions.logType === "stdout") {
|
|
1586
|
-
console.log(
|
|
1587
|
-
console.log(
|
|
2106
|
+
console.log(chalk4.green.bold(`\uD83D\uDCC4 Tailing stdout for ${options.name}:`));
|
|
2107
|
+
console.log(chalk4.gray("\u2550".repeat(50)));
|
|
1588
2108
|
try {
|
|
1589
2109
|
await waitForLogReady(currentProcess.stdout_path);
|
|
1590
2110
|
} catch (err) {
|
|
1591
|
-
console.warn(
|
|
2111
|
+
console.warn(chalk4.yellow(`\u26A0\uFE0F Stdout log not ready yet for ${options.name}\u2014starting tail anyway: ${err.message}`));
|
|
1592
2112
|
}
|
|
1593
|
-
const stop = tailFile(currentProcess.stdout_path, "",
|
|
2113
|
+
const stop = tailFile(currentProcess.stdout_path, "", chalk4.white, logOptions.lines);
|
|
1594
2114
|
stops.push(stop);
|
|
1595
2115
|
}
|
|
1596
2116
|
if (logOptions.logType === "both" || logOptions.logType === "stderr") {
|
|
1597
|
-
console.log(
|
|
1598
|
-
console.log(
|
|
2117
|
+
console.log(chalk4.red.bold(`\uD83D\uDCC4 Tailing stderr for ${options.name}:`));
|
|
2118
|
+
console.log(chalk4.gray("\u2550".repeat(50)));
|
|
1599
2119
|
try {
|
|
1600
2120
|
await waitForLogReady(currentProcess.stderr_path);
|
|
1601
2121
|
} catch (err) {
|
|
1602
|
-
console.warn(
|
|
2122
|
+
console.warn(chalk4.yellow(`\u26A0\uFE0F Stderr log not ready yet for ${options.name}\u2014starting tail anyway: ${err.message}`));
|
|
1603
2123
|
}
|
|
1604
|
-
const stop = tailFile(currentProcess.stderr_path, "",
|
|
2124
|
+
const stop = tailFile(currentProcess.stderr_path, "", chalk4.red, logOptions.lines);
|
|
1605
2125
|
stops.push(stop);
|
|
1606
2126
|
}
|
|
1607
2127
|
return stops;
|
|
@@ -1626,7 +2146,7 @@ ${color(content)}
|
|
|
1626
2146
|
const died = await dumpLogsIfDead(currentProcess, restartReason);
|
|
1627
2147
|
if (died) {
|
|
1628
2148
|
if (lastRestartPath) {
|
|
1629
|
-
console.log(
|
|
2149
|
+
console.log(chalk4.yellow(`\u26A0\uFE0F Compile error on change\u2014pausing restarts until manual fix.`));
|
|
1630
2150
|
return;
|
|
1631
2151
|
} else {
|
|
1632
2152
|
error(`Failed to start process '${options.name}'. Aborting watch mode.`);
|
|
@@ -1639,7 +2159,7 @@ ${color(content)}
|
|
|
1639
2159
|
} finally {
|
|
1640
2160
|
isRestarting = false;
|
|
1641
2161
|
if (currentProcess) {
|
|
1642
|
-
console.log(
|
|
2162
|
+
console.log(chalk4.cyan(`
|
|
1643
2163
|
\uD83D\uDC40 Watching for file changes in: ${currentProcess.workdir}`));
|
|
1644
2164
|
}
|
|
1645
2165
|
}
|
|
@@ -1659,7 +2179,7 @@ ${color(content)}
|
|
|
1659
2179
|
}
|
|
1660
2180
|
tailStops = await startTails();
|
|
1661
2181
|
const workdir = currentProcess.workdir;
|
|
1662
|
-
console.log(
|
|
2182
|
+
console.log(chalk4.cyan(`
|
|
1663
2183
|
\uD83D\uDC40 Watching for file changes in: ${workdir}`));
|
|
1664
2184
|
const watcher = fs4.watch(workdir, { recursive: true }, (eventType, filename) => {
|
|
1665
2185
|
if (filename == null)
|
|
@@ -1672,7 +2192,7 @@ ${color(content)}
|
|
|
1672
2192
|
debounceTimeout = setTimeout(() => restartProcess(fullPath), 500);
|
|
1673
2193
|
});
|
|
1674
2194
|
const cleanup = async () => {
|
|
1675
|
-
console.log(
|
|
2195
|
+
console.log(chalk4.magenta(`
|
|
1676
2196
|
SIGINT received...`));
|
|
1677
2197
|
watcher.close();
|
|
1678
2198
|
tailStops.forEach((stop) => stop());
|
|
@@ -1695,7 +2215,7 @@ SIGINT received...`));
|
|
|
1695
2215
|
init_db();
|
|
1696
2216
|
init_logger();
|
|
1697
2217
|
init_platform();
|
|
1698
|
-
import
|
|
2218
|
+
import chalk5 from "chalk";
|
|
1699
2219
|
import * as fs5 from "fs";
|
|
1700
2220
|
async function showLogs(name, logType = "both", lines) {
|
|
1701
2221
|
const proc = getProcess(name);
|
|
@@ -1704,17 +2224,17 @@ async function showLogs(name, logType = "both", lines) {
|
|
|
1704
2224
|
return;
|
|
1705
2225
|
}
|
|
1706
2226
|
if (logType === "both" || logType === "stdout") {
|
|
1707
|
-
console.log(
|
|
1708
|
-
console.log(
|
|
2227
|
+
console.log(chalk5.green.bold(`\uD83D\uDCC4 Stdout logs for ${name}:`));
|
|
2228
|
+
console.log(chalk5.gray("\u2550".repeat(50)));
|
|
1709
2229
|
if (fs5.existsSync(proc.stdout_path)) {
|
|
1710
2230
|
try {
|
|
1711
2231
|
const output = await readFileTail(proc.stdout_path, lines);
|
|
1712
|
-
console.log(output ||
|
|
2232
|
+
console.log(output || chalk5.gray("(no output)"));
|
|
1713
2233
|
} catch (err) {
|
|
1714
|
-
console.log(
|
|
2234
|
+
console.log(chalk5.red(`Error reading stdout: ${err}`));
|
|
1715
2235
|
}
|
|
1716
2236
|
} else {
|
|
1717
|
-
console.log(
|
|
2237
|
+
console.log(chalk5.gray("(log file not found)"));
|
|
1718
2238
|
}
|
|
1719
2239
|
if (logType === "both") {
|
|
1720
2240
|
console.log(`
|
|
@@ -1722,17 +2242,17 @@ async function showLogs(name, logType = "both", lines) {
|
|
|
1722
2242
|
}
|
|
1723
2243
|
}
|
|
1724
2244
|
if (logType === "both" || logType === "stderr") {
|
|
1725
|
-
console.log(
|
|
1726
|
-
console.log(
|
|
2245
|
+
console.log(chalk5.red.bold(`\uD83D\uDCC4 Stderr logs for ${name}:`));
|
|
2246
|
+
console.log(chalk5.gray("\u2550".repeat(50)));
|
|
1727
2247
|
if (fs5.existsSync(proc.stderr_path)) {
|
|
1728
2248
|
try {
|
|
1729
2249
|
const output = await readFileTail(proc.stderr_path, lines);
|
|
1730
|
-
console.log(output ||
|
|
2250
|
+
console.log(output || chalk5.gray("(no errors)"));
|
|
1731
2251
|
} catch (err) {
|
|
1732
|
-
console.log(
|
|
2252
|
+
console.log(chalk5.red(`Error reading stderr: ${err}`));
|
|
1733
2253
|
}
|
|
1734
2254
|
} else {
|
|
1735
|
-
console.log(
|
|
2255
|
+
console.log(chalk5.gray("(log file not found)"));
|
|
1736
2256
|
}
|
|
1737
2257
|
}
|
|
1738
2258
|
}
|
|
@@ -1742,35 +2262,44 @@ init_logger();
|
|
|
1742
2262
|
init_db();
|
|
1743
2263
|
init_utils();
|
|
1744
2264
|
init_platform();
|
|
1745
|
-
import
|
|
2265
|
+
import chalk6 from "chalk";
|
|
1746
2266
|
async function showDetails(name) {
|
|
1747
2267
|
const proc = getProcess(name);
|
|
1748
2268
|
if (!proc) {
|
|
1749
2269
|
error(`No process found named '${name}'`);
|
|
1750
2270
|
return;
|
|
1751
2271
|
}
|
|
1752
|
-
|
|
2272
|
+
let isRunning = await isProcessRunning(proc.pid, proc.command);
|
|
2273
|
+
if (!isRunning && proc.pid > 0) {
|
|
2274
|
+
const reconciled = await reconcileProcessPids([{ name: proc.name, pid: proc.pid, command: proc.command, workdir: proc.workdir }], new Set([proc.pid]));
|
|
2275
|
+
const newPid = reconciled.get(proc.name);
|
|
2276
|
+
if (newPid) {
|
|
2277
|
+
updateProcessPid(proc.name, newPid);
|
|
2278
|
+
proc.pid = newPid;
|
|
2279
|
+
isRunning = true;
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
1753
2282
|
const runtime = calculateRuntime(proc.timestamp);
|
|
1754
2283
|
const envVars = parseEnvString(proc.env);
|
|
1755
2284
|
const ports = isRunning ? await getProcessPorts(proc.pid) : [];
|
|
1756
|
-
const portDisplay = ports.length > 0 ? ports.map((p) =>
|
|
2285
|
+
const portDisplay = ports.length > 0 ? ports.map((p) => chalk6.hex("#FF6B6B")(`:${p}`)).join(", ") : null;
|
|
1757
2286
|
const details = `
|
|
1758
|
-
${
|
|
1759
|
-
${
|
|
1760
|
-
${
|
|
1761
|
-
${
|
|
1762
|
-
${
|
|
1763
|
-
${
|
|
1764
|
-
${
|
|
1765
|
-
${
|
|
1766
|
-
${
|
|
1767
|
-
${
|
|
1768
|
-
${
|
|
1769
|
-
${
|
|
2287
|
+
${chalk6.bold("Process Details:")}
|
|
2288
|
+
${chalk6.gray("\u2550".repeat(50))}
|
|
2289
|
+
${chalk6.cyan.bold("Name:")} ${proc.name}
|
|
2290
|
+
${chalk6.yellow.bold("PID:")} ${proc.pid}${portDisplay ? `
|
|
2291
|
+
${chalk6.hex("#FF6B6B").bold("Port:")} ${portDisplay}` : ""}
|
|
2292
|
+
${chalk6.bold("Status:")} ${isRunning ? chalk6.green.bold("\u25CF Running") : chalk6.red.bold("\u25CB Stopped")}
|
|
2293
|
+
${chalk6.magenta.bold("Runtime:")} ${runtime}
|
|
2294
|
+
${chalk6.blue.bold("Working Directory:")} ${proc.workdir}
|
|
2295
|
+
${chalk6.white.bold("Command:")} ${proc.command}
|
|
2296
|
+
${chalk6.gray.bold("Config Path:")} ${proc.configPath}
|
|
2297
|
+
${chalk6.green.bold("Stdout Path:")} ${proc.stdout_path}
|
|
2298
|
+
${chalk6.red.bold("Stderr Path:")} ${proc.stderr_path}
|
|
1770
2299
|
|
|
1771
|
-
${
|
|
1772
|
-
${
|
|
1773
|
-
${Object.entries(envVars).map(([key, value]) => `${
|
|
2300
|
+
${chalk6.bold("\uD83D\uDD27 Environment Variables:")}
|
|
2301
|
+
${chalk6.gray("\u2550".repeat(50))}
|
|
2302
|
+
${Object.entries(envVars).map(([key, value]) => `${chalk6.cyan.bold(key)} = ${chalk6.yellow(value)}`).join(`
|
|
1774
2303
|
`)}
|
|
1775
2304
|
`;
|
|
1776
2305
|
announce(details, `Process Details: ${name}`);
|
|
@@ -1781,8 +2310,8 @@ init_logger();
|
|
|
1781
2310
|
init_platform();
|
|
1782
2311
|
init_db();
|
|
1783
2312
|
import dedent from "dedent";
|
|
1784
|
-
import
|
|
1785
|
-
import { join as
|
|
2313
|
+
import chalk7 from "chalk";
|
|
2314
|
+
import { join as join4 } from "path";
|
|
1786
2315
|
var {sleep: sleep3 } = globalThis.Bun;
|
|
1787
2316
|
import { configure } from "measure-fn";
|
|
1788
2317
|
if (!Bun.argv.includes("--_serve")) {
|
|
@@ -1790,15 +2319,55 @@ if (!Bun.argv.includes("--_serve")) {
|
|
|
1790
2319
|
configure({ silent: true });
|
|
1791
2320
|
}
|
|
1792
2321
|
}
|
|
2322
|
+
function redirectConsoleToFiles() {
|
|
2323
|
+
const stdoutPath = Bun.env.BGR_STDOUT;
|
|
2324
|
+
const stderrPath = Bun.env.BGR_STDERR;
|
|
2325
|
+
if (!stdoutPath && !stderrPath)
|
|
2326
|
+
return;
|
|
2327
|
+
const { appendFileSync } = __require("fs");
|
|
2328
|
+
const stripAnsi2 = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
2329
|
+
const timestamp = () => new Date().toISOString().replace("T", " ").substring(0, 19);
|
|
2330
|
+
if (stdoutPath) {
|
|
2331
|
+
const origLog = console.log;
|
|
2332
|
+
const origWarn = console.warn;
|
|
2333
|
+
console.log = (...args) => {
|
|
2334
|
+
const line = `[${timestamp()}] ${stripAnsi2(args.map(String).join(" "))}
|
|
2335
|
+
`;
|
|
2336
|
+
try {
|
|
2337
|
+
appendFileSync(stdoutPath, line);
|
|
2338
|
+
} catch {}
|
|
2339
|
+
origLog.apply(console, args);
|
|
2340
|
+
};
|
|
2341
|
+
console.warn = (...args) => {
|
|
2342
|
+
const line = `[${timestamp()}] WARN: ${stripAnsi2(args.map(String).join(" "))}
|
|
2343
|
+
`;
|
|
2344
|
+
try {
|
|
2345
|
+
appendFileSync(stdoutPath, line);
|
|
2346
|
+
} catch {}
|
|
2347
|
+
origWarn.apply(console, args);
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
if (stderrPath) {
|
|
2351
|
+
const origError = console.error;
|
|
2352
|
+
console.error = (...args) => {
|
|
2353
|
+
const line = `[${timestamp()}] ERROR: ${stripAnsi2(args.map(String).join(" "))}
|
|
2354
|
+
`;
|
|
2355
|
+
try {
|
|
2356
|
+
appendFileSync(stderrPath, line);
|
|
2357
|
+
} catch {}
|
|
2358
|
+
origError.apply(console, args);
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
1793
2362
|
async function showHelp() {
|
|
1794
2363
|
const usage = dedent`
|
|
1795
|
-
${
|
|
1796
|
-
${
|
|
2364
|
+
${chalk7.bold("bgrun \u2014 Bun Background Runner")}
|
|
2365
|
+
${chalk7.gray("\u2550".repeat(50))}
|
|
1797
2366
|
|
|
1798
|
-
${
|
|
2367
|
+
${chalk7.yellow("Usage:")}
|
|
1799
2368
|
bgrun [name] [options]
|
|
1800
2369
|
|
|
1801
|
-
${
|
|
2370
|
+
${chalk7.yellow("Commands:")}
|
|
1802
2371
|
bgrun List all processes
|
|
1803
2372
|
bgrun [name] Show details for a process
|
|
1804
2373
|
bgrun --dashboard Launch web dashboard (managed by bgrun)
|
|
@@ -1811,7 +2380,7 @@ async function showHelp() {
|
|
|
1811
2380
|
bgrun --clean Remove all stopped processes
|
|
1812
2381
|
bgrun --nuke Delete ALL processes
|
|
1813
2382
|
|
|
1814
|
-
${
|
|
2383
|
+
${chalk7.yellow("Options:")}
|
|
1815
2384
|
--name <string> Process name (required for new)
|
|
1816
2385
|
--command <string> Process command (required for new)
|
|
1817
2386
|
--directory <path> Working directory (required for new)
|
|
@@ -1831,7 +2400,7 @@ async function showHelp() {
|
|
|
1831
2400
|
--port <number> Port for dashboard (default: 3000)
|
|
1832
2401
|
--help Show this help message
|
|
1833
2402
|
|
|
1834
|
-
${
|
|
2403
|
+
${chalk7.yellow("Examples:")}
|
|
1835
2404
|
bgrun --dashboard
|
|
1836
2405
|
bgrun --name myapp --command "bun run dev" --directory . --watch
|
|
1837
2406
|
bgrun myapp --logs --lines 50
|
|
@@ -1878,11 +2447,13 @@ async function run2() {
|
|
|
1878
2447
|
allowPositionals: true
|
|
1879
2448
|
});
|
|
1880
2449
|
if (values["_serve"]) {
|
|
2450
|
+
redirectConsoleToFiles();
|
|
1881
2451
|
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|
|
1882
2452
|
await startServer2();
|
|
1883
2453
|
return;
|
|
1884
2454
|
}
|
|
1885
2455
|
if (values["_guard-loop"]) {
|
|
2456
|
+
redirectConsoleToFiles();
|
|
1886
2457
|
const { startGuardLoop: startGuardLoop2 } = await Promise.resolve().then(() => (init_guard(), exports_guard));
|
|
1887
2458
|
const intervalStr = positionals[0];
|
|
1888
2459
|
const intervalMs = intervalStr ? parseInt(intervalStr) * 1000 : undefined;
|
|
@@ -1892,7 +2463,7 @@ async function run2() {
|
|
|
1892
2463
|
if (values.dashboard) {
|
|
1893
2464
|
const dashboardName = "bgr-dashboard";
|
|
1894
2465
|
const homePath3 = getHomeDir();
|
|
1895
|
-
const bgrDir2 =
|
|
2466
|
+
const bgrDir2 = join4(homePath3, ".bgr");
|
|
1896
2467
|
const requestedPort = values.port;
|
|
1897
2468
|
const existing = getProcess(dashboardName);
|
|
1898
2469
|
if (existing && await isProcessRunning(existing.pid)) {
|
|
@@ -1906,10 +2477,10 @@ async function run2() {
|
|
|
1906
2477
|
const portStr = existingPorts.length > 0 ? `:${existingPorts[0]}` : "(detecting...)";
|
|
1907
2478
|
announce(`Dashboard is already running (PID ${existing.pid})
|
|
1908
2479
|
|
|
1909
|
-
` + ` \uD83C\uDF10 ${
|
|
2480
|
+
` + ` \uD83C\uDF10 ${chalk7.cyan(`http://localhost${portStr}`)}
|
|
1910
2481
|
|
|
1911
|
-
Use ${
|
|
1912
|
-
Use ${
|
|
2482
|
+
Use ${chalk7.yellow(`bgrun --stop ${dashboardName}`)} to stop it
|
|
2483
|
+
Use ${chalk7.yellow(`bgrun --dashboard --force`)} to restart`, "BGR Dashboard");
|
|
1913
2484
|
return;
|
|
1914
2485
|
}
|
|
1915
2486
|
if (existing) {
|
|
@@ -1927,35 +2498,39 @@ async function run2() {
|
|
|
1927
2498
|
const scriptPath = resolve(process.argv[1]);
|
|
1928
2499
|
const spawnCommand = `bun run ${scriptPath} --_serve`;
|
|
1929
2500
|
const command = `bgrun --_serve`;
|
|
1930
|
-
const stdoutPath =
|
|
1931
|
-
const stderrPath =
|
|
2501
|
+
const stdoutPath = join4(bgrDir2, `${dashboardName}-out.txt`);
|
|
2502
|
+
const stderrPath = join4(bgrDir2, `${dashboardName}-err.txt`);
|
|
1932
2503
|
await Bun.write(stdoutPath, "");
|
|
1933
2504
|
await Bun.write(stderrPath, "");
|
|
1934
2505
|
const spawnEnv = { ...Bun.env };
|
|
1935
2506
|
if (requestedPort) {
|
|
1936
2507
|
spawnEnv.BUN_PORT = requestedPort;
|
|
1937
2508
|
}
|
|
2509
|
+
spawnEnv.BGR_STDOUT = stdoutPath;
|
|
2510
|
+
spawnEnv.BGR_STDERR = stderrPath;
|
|
1938
2511
|
const targetPort = parseInt(requestedPort || Bun.env.BUN_PORT || "3000");
|
|
1939
2512
|
if (!isNaN(targetPort) && targetPort > 0) {
|
|
1940
2513
|
const portFree = await isPortFree(targetPort);
|
|
1941
2514
|
if (!portFree) {
|
|
1942
|
-
console.log(
|
|
2515
|
+
console.log(chalk7.yellow(` \u26A1 Port ${targetPort} is occupied \u2014 reclaiming...`));
|
|
1943
2516
|
await killProcessOnPort(targetPort);
|
|
1944
2517
|
const freed = await waitForPortFree(targetPort, 5000);
|
|
1945
2518
|
if (!freed) {
|
|
1946
|
-
console.log(
|
|
2519
|
+
console.log(chalk7.red(` \u26A0 Could not free port ${targetPort} \u2014 dashboard may pick a fallback port`));
|
|
1947
2520
|
}
|
|
1948
2521
|
}
|
|
1949
2522
|
}
|
|
1950
2523
|
const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
|
|
1951
2524
|
env: spawnEnv,
|
|
1952
2525
|
cwd: bgrDir2,
|
|
1953
|
-
stdout:
|
|
1954
|
-
stderr:
|
|
2526
|
+
stdout: "ignore",
|
|
2527
|
+
stderr: "ignore",
|
|
2528
|
+
detached: true
|
|
1955
2529
|
});
|
|
1956
2530
|
newProcess.unref();
|
|
1957
2531
|
await sleep3(2000);
|
|
1958
|
-
const
|
|
2532
|
+
const resolvedPort = parseInt(requestedPort || Bun.env.BUN_PORT || "3000");
|
|
2533
|
+
const actualPid = await findPidByPort(resolvedPort, 1e4) ?? await findChildPid(newProcess.pid);
|
|
1959
2534
|
let actualPort = null;
|
|
1960
2535
|
for (let attempt = 0;attempt < 10; attempt++) {
|
|
1961
2536
|
const ports = await getProcessPorts(actualPid);
|
|
@@ -1978,19 +2553,19 @@ async function run2() {
|
|
|
1978
2553
|
const portDisplay = actualPort ? String(actualPort) : "(detecting...)";
|
|
1979
2554
|
const urlDisplay = actualPort ? `http://localhost:${actualPort}` : "http://localhost (port auto-assigned)";
|
|
1980
2555
|
const msg = dedent`
|
|
1981
|
-
${
|
|
1982
|
-
${
|
|
2556
|
+
${chalk7.bold("\u26A1 BGR Dashboard launched")}
|
|
2557
|
+
${chalk7.gray("\u2500".repeat(40))}
|
|
1983
2558
|
|
|
1984
|
-
\u{1f310} Open in browser: ${
|
|
2559
|
+
\u{1f310} Open in browser: ${chalk7.cyan.underline(urlDisplay)}
|
|
1985
2560
|
\u{1f4ca} Manage all your processes from the web UI
|
|
1986
2561
|
\u{1f504} Auto-refreshes every 3 seconds
|
|
1987
2562
|
|
|
1988
|
-
${
|
|
1989
|
-
Process: ${
|
|
2563
|
+
${chalk7.gray("\u2500".repeat(40))}
|
|
2564
|
+
Process: ${chalk7.white(dashboardName)} | PID: ${chalk7.white(String(actualPid))} | Port: ${chalk7.white(portDisplay)}
|
|
1990
2565
|
|
|
1991
|
-
${
|
|
1992
|
-
${
|
|
1993
|
-
${
|
|
2566
|
+
${chalk7.yellow("bgrun bgr-dashboard --logs")} View dashboard logs
|
|
2567
|
+
${chalk7.yellow("bgrun --stop bgr-dashboard")} Stop the dashboard
|
|
2568
|
+
${chalk7.yellow("bgrun --restart bgr-dashboard")} Restart the dashboard
|
|
1994
2569
|
`;
|
|
1995
2570
|
announce(msg, "BGR Dashboard");
|
|
1996
2571
|
return;
|
|
@@ -1998,13 +2573,13 @@ async function run2() {
|
|
|
1998
2573
|
if (values.guard) {
|
|
1999
2574
|
const guardName = "bgr-guard";
|
|
2000
2575
|
const homePath3 = getHomeDir();
|
|
2001
|
-
const bgrDir2 =
|
|
2576
|
+
const bgrDir2 = join4(homePath3, ".bgr");
|
|
2002
2577
|
const existing = getProcess(guardName);
|
|
2003
2578
|
if (existing && await isProcessRunning(existing.pid)) {
|
|
2004
2579
|
announce(`Guard is already running (PID ${existing.pid})
|
|
2005
2580
|
|
|
2006
|
-
Use ${
|
|
2007
|
-
Use ${
|
|
2581
|
+
Use ${chalk7.yellow(`bgrun --stop ${guardName}`)} to stop it
|
|
2582
|
+
Use ${chalk7.yellow(`bgrun --guard --force`)} to restart`, "BGR Guard");
|
|
2008
2583
|
return;
|
|
2009
2584
|
}
|
|
2010
2585
|
if (existing) {
|
|
@@ -2017,19 +2592,27 @@ async function run2() {
|
|
|
2017
2592
|
const scriptPath = resolve(process.argv[1]);
|
|
2018
2593
|
const spawnCommand = `bun run ${scriptPath} --_guard-loop`;
|
|
2019
2594
|
const command = `bgrun --_guard-loop`;
|
|
2020
|
-
const stdoutPath =
|
|
2021
|
-
const stderrPath =
|
|
2595
|
+
const stdoutPath = join4(bgrDir2, `${guardName}-out.txt`);
|
|
2596
|
+
const stderrPath = join4(bgrDir2, `${guardName}-err.txt`);
|
|
2022
2597
|
await Bun.write(stdoutPath, "");
|
|
2023
2598
|
await Bun.write(stderrPath, "");
|
|
2024
2599
|
const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
|
|
2025
|
-
env: { ...Bun.env },
|
|
2600
|
+
env: { ...Bun.env, BGR_STDOUT: stdoutPath, BGR_STDERR: stderrPath },
|
|
2026
2601
|
cwd: bgrDir2,
|
|
2027
|
-
stdout:
|
|
2028
|
-
stderr:
|
|
2602
|
+
stdout: "ignore",
|
|
2603
|
+
stderr: "ignore",
|
|
2604
|
+
detached: true
|
|
2029
2605
|
});
|
|
2030
2606
|
newProcess.unref();
|
|
2031
2607
|
await sleep3(1000);
|
|
2032
|
-
|
|
2608
|
+
let actualPid = await findChildPid(newProcess.pid);
|
|
2609
|
+
if (!await isProcessRunning(actualPid)) {
|
|
2610
|
+
const { psExec: ps } = await Promise.resolve().then(() => (init_platform(), exports_platform));
|
|
2611
|
+
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);
|
|
2612
|
+
const foundPid = parseInt(result.trim());
|
|
2613
|
+
if (!isNaN(foundPid) && foundPid > 0)
|
|
2614
|
+
actualPid = foundPid;
|
|
2615
|
+
}
|
|
2033
2616
|
await retryDatabaseOperation(() => insertProcess({
|
|
2034
2617
|
pid: actualPid,
|
|
2035
2618
|
workdir: bgrDir2,
|
|
@@ -2041,19 +2624,19 @@ async function run2() {
|
|
|
2041
2624
|
stderr_path: stderrPath
|
|
2042
2625
|
}));
|
|
2043
2626
|
const msg = dedent`
|
|
2044
|
-
${
|
|
2045
|
-
${
|
|
2627
|
+
${chalk7.bold("\uD83D\uDEE1\uFE0F BGR Standalone Guard launched")}
|
|
2628
|
+
${chalk7.gray("\u2500".repeat(40))}
|
|
2046
2629
|
|
|
2047
2630
|
Monitors: All processes with BGR_KEEP_ALIVE=true
|
|
2048
2631
|
Also watches: bgr-dashboard (auto-restart if it dies)
|
|
2049
2632
|
Check interval: 30 seconds
|
|
2050
2633
|
Backoff: Exponential after 5 rapid crashes
|
|
2051
2634
|
|
|
2052
|
-
${
|
|
2053
|
-
Process: ${
|
|
2635
|
+
${chalk7.gray("\u2500".repeat(40))}
|
|
2636
|
+
Process: ${chalk7.white(guardName)} | PID: ${chalk7.white(String(actualPid))}
|
|
2054
2637
|
|
|
2055
|
-
${
|
|
2056
|
-
${
|
|
2638
|
+
${chalk7.yellow(`bgrun ${guardName} --logs`)} View guard logs
|
|
2639
|
+
${chalk7.yellow(`bgrun --stop ${guardName}`)} Stop the guard
|
|
2057
2640
|
`;
|
|
2058
2641
|
announce(msg, "BGR Guard");
|
|
2059
2642
|
return;
|
|
@@ -2070,13 +2653,13 @@ async function run2() {
|
|
|
2070
2653
|
const info = getDbInfo();
|
|
2071
2654
|
const version = await getVersion();
|
|
2072
2655
|
console.log(dedent`
|
|
2073
|
-
${
|
|
2074
|
-
${
|
|
2075
|
-
Version: ${
|
|
2076
|
-
BGR Home: ${
|
|
2077
|
-
DB Path: ${
|
|
2656
|
+
${chalk7.bold("bgrun debug info")}
|
|
2657
|
+
${chalk7.gray("\u2500".repeat(40))}
|
|
2658
|
+
Version: ${chalk7.cyan(version)}
|
|
2659
|
+
BGR Home: ${chalk7.yellow(info.bgrHome)}
|
|
2660
|
+
DB Path: ${chalk7.yellow(info.dbPath)}
|
|
2078
2661
|
DB File: ${info.dbFilename}
|
|
2079
|
-
DB Exists: ${info.exists ?
|
|
2662
|
+
DB Exists: ${info.exists ? chalk7.green("\u2713") : chalk7.red("\u2717")}
|
|
2080
2663
|
Platform: ${process.platform}
|
|
2081
2664
|
Bun: ${Bun.version}
|
|
2082
2665
|
`);
|
|
@@ -2097,12 +2680,12 @@ async function run2() {
|
|
|
2097
2680
|
error("No processes registered.");
|
|
2098
2681
|
return;
|
|
2099
2682
|
}
|
|
2100
|
-
console.log(
|
|
2683
|
+
console.log(chalk7.bold(`
|
|
2101
2684
|
Restarting ${all.length} processes...
|
|
2102
2685
|
`));
|
|
2103
2686
|
for (const proc of all) {
|
|
2104
2687
|
try {
|
|
2105
|
-
console.log(
|
|
2688
|
+
console.log(chalk7.yellow(` \u21BB Restarting ${proc.name}...`));
|
|
2106
2689
|
await handleRun({
|
|
2107
2690
|
action: "run",
|
|
2108
2691
|
name: proc.name,
|
|
@@ -2110,10 +2693,10 @@ async function run2() {
|
|
|
2110
2693
|
remoteName: ""
|
|
2111
2694
|
});
|
|
2112
2695
|
} catch (err) {
|
|
2113
|
-
console.error(
|
|
2696
|
+
console.error(chalk7.red(` \u2717 Failed to restart ${proc.name}: ${err.message}`));
|
|
2114
2697
|
}
|
|
2115
2698
|
}
|
|
2116
|
-
console.log(
|
|
2699
|
+
console.log(chalk7.green(`
|
|
2117
2700
|
\u2713 All processes restarted.
|
|
2118
2701
|
`));
|
|
2119
2702
|
return;
|
|
@@ -2125,22 +2708,22 @@ async function run2() {
|
|
|
2125
2708
|
error("No processes registered.");
|
|
2126
2709
|
return;
|
|
2127
2710
|
}
|
|
2128
|
-
console.log(
|
|
2711
|
+
console.log(chalk7.bold(`
|
|
2129
2712
|
Stopping ${all.length} processes...
|
|
2130
2713
|
`));
|
|
2131
2714
|
for (const proc of all) {
|
|
2132
2715
|
try {
|
|
2133
2716
|
if (await isProcessRunning(proc.pid)) {
|
|
2134
|
-
console.log(
|
|
2717
|
+
console.log(chalk7.yellow(` \u25A0 Stopping ${proc.name} (PID ${proc.pid})...`));
|
|
2135
2718
|
await handleStop(proc.name);
|
|
2136
2719
|
} else {
|
|
2137
|
-
console.log(
|
|
2720
|
+
console.log(chalk7.gray(` \u25CB ${proc.name} already stopped`));
|
|
2138
2721
|
}
|
|
2139
2722
|
} catch (err) {
|
|
2140
|
-
console.error(
|
|
2723
|
+
console.error(chalk7.red(` \u2717 Failed to stop ${proc.name}: ${err.message}`));
|
|
2141
2724
|
}
|
|
2142
2725
|
}
|
|
2143
|
-
console.log(
|
|
2726
|
+
console.log(chalk7.green(`
|
|
2144
2727
|
\u2713 All processes stopped.
|
|
2145
2728
|
`));
|
|
2146
2729
|
return;
|
|
@@ -2201,6 +2784,13 @@ async function run2() {
|
|
|
2201
2784
|
});
|
|
2202
2785
|
return;
|
|
2203
2786
|
}
|
|
2787
|
+
if (name === "list") {
|
|
2788
|
+
await showAll({
|
|
2789
|
+
json: values.json,
|
|
2790
|
+
filter: values.filter
|
|
2791
|
+
});
|
|
2792
|
+
return;
|
|
2793
|
+
}
|
|
2204
2794
|
if (name) {
|
|
2205
2795
|
if (!values.command && !values.directory) {
|
|
2206
2796
|
await showDetails(name);
|
|
@@ -2230,5 +2820,8 @@ async function run2() {
|
|
|
2230
2820
|
}
|
|
2231
2821
|
}
|
|
2232
2822
|
run2().catch((err) => {
|
|
2233
|
-
|
|
2823
|
+
if (err.name !== "BgrunError") {
|
|
2824
|
+
console.error(err);
|
|
2825
|
+
}
|
|
2826
|
+
process.exit(1);
|
|
2234
2827
|
});
|