bgrun 3.11.0 → 3.12.1
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 +8 -2
- package/dashboard/app/api/events/route.ts +10 -2
- 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/processes/route.ts +11 -3
- package/dashboard/app/api/restart/[name]/route.ts +7 -0
- package/dashboard/app/api/start/route.ts +5 -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 +545 -29
- package/dashboard/app/page.client.tsx +717 -61
- package/dashboard/app/page.tsx +130 -0
- package/dist/index.js +663 -184
- package/package.json +4 -3
- package/scripts/bgr-startup.ps1 +118 -0
- package/scripts/bgrun-startup.ps1 +91 -0
- package/src/bgrun.test.ts +109 -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 +115 -0
- package/src/guard.ts +51 -0
- package/src/index.ts +83 -14
- package/src/index_copy.ts +614 -0
- package/src/logger.ts +12 -2
- package/src/platform.ts +87 -50
- package/src/server.ts +87 -3
- package/src/table.ts +3 -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,12 @@ 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
|
+
return false;
|
|
84
|
+
}
|
|
38
85
|
} else {
|
|
39
86
|
const result = await $`ps -p ${pid}`.nothrow().text();
|
|
40
87
|
return result.includes(`${pid}`);
|
|
@@ -66,7 +113,7 @@ async function isDockerContainerRunning(command) {
|
|
|
66
113
|
async function getChildPids(pid) {
|
|
67
114
|
try {
|
|
68
115
|
if (isWindows()) {
|
|
69
|
-
const result = await
|
|
116
|
+
const result = await psExec(`Get-CimInstance Win32_Process -Filter 'ParentProcessId=${pid}' | Select-Object -ExpandProperty ProcessId`, 3000);
|
|
70
117
|
return result.split(`
|
|
71
118
|
`).map((line) => parseInt(line.trim())).filter((n) => !isNaN(n) && n > 0);
|
|
72
119
|
} else {
|
|
@@ -125,6 +172,35 @@ async function isPortFree(port) {
|
|
|
125
172
|
return true;
|
|
126
173
|
}
|
|
127
174
|
}
|
|
175
|
+
async function getPortInfo(port) {
|
|
176
|
+
try {
|
|
177
|
+
if (isWindows()) {
|
|
178
|
+
const result = await $`netstat -ano | findstr :${port}`.nothrow().quiet().text();
|
|
179
|
+
for (const line of result.split(`
|
|
180
|
+
`)) {
|
|
181
|
+
const match = line.match(new RegExp(`:(${port})\\s+.*LISTENING\\s+(\\d+)`));
|
|
182
|
+
if (match) {
|
|
183
|
+
const pid = parseInt(match[2]);
|
|
184
|
+
if (pid > 0 && await isProcessRunning(pid)) {
|
|
185
|
+
const nameResult = await $`powershell -NoProfile -Command "(Get-Process -Id ${pid} -ErrorAction SilentlyContinue).ProcessName"`.nothrow().quiet().text();
|
|
186
|
+
return { inUse: true, pid, processName: nameResult.trim() || "unknown" };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return { inUse: false };
|
|
191
|
+
} else {
|
|
192
|
+
const result = await $`ss -tln sport = :${port}`.nothrow().quiet().text();
|
|
193
|
+
const lines = result.trim().split(`
|
|
194
|
+
`).filter((l) => l.trim());
|
|
195
|
+
if (lines.length > 1) {
|
|
196
|
+
return { inUse: true };
|
|
197
|
+
}
|
|
198
|
+
return { inUse: false };
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
return { inUse: false };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
128
204
|
async function waitForPortFree(port, timeoutMs = 5000) {
|
|
129
205
|
const startTime = Date.now();
|
|
130
206
|
const pollInterval = 300;
|
|
@@ -188,12 +264,12 @@ function getShellCommand(command) {
|
|
|
188
264
|
}
|
|
189
265
|
async function findChildPid(parentPid) {
|
|
190
266
|
let currentPid = parentPid;
|
|
191
|
-
const maxDepth =
|
|
267
|
+
const maxDepth = 2;
|
|
192
268
|
for (let depth = 0;depth < maxDepth; depth++) {
|
|
193
269
|
try {
|
|
194
270
|
let childPids = [];
|
|
195
271
|
if (isWindows()) {
|
|
196
|
-
const result = await
|
|
272
|
+
const result = await psExec(`Get-CimInstance Win32_Process -Filter 'ParentProcessId=${currentPid}' | Select-Object -ExpandProperty ProcessId`, 3000);
|
|
197
273
|
childPids = result.split(`
|
|
198
274
|
`).map((line) => parseInt(line.trim())).filter((n) => !isNaN(n) && n > 0);
|
|
199
275
|
} else {
|
|
@@ -201,9 +277,8 @@ async function findChildPid(parentPid) {
|
|
|
201
277
|
childPids = result.trim().split(`
|
|
202
278
|
`).map((line) => parseInt(line.trim())).filter((n) => !isNaN(n) && n > 0);
|
|
203
279
|
}
|
|
204
|
-
if (childPids.length === 0)
|
|
280
|
+
if (childPids.length === 0)
|
|
205
281
|
break;
|
|
206
|
-
}
|
|
207
282
|
currentPid = childPids[0];
|
|
208
283
|
} catch {
|
|
209
284
|
break;
|
|
@@ -211,6 +286,112 @@ async function findChildPid(parentPid) {
|
|
|
211
286
|
}
|
|
212
287
|
return currentPid;
|
|
213
288
|
}
|
|
289
|
+
async function reconcileProcessPids(processes, deadPids) {
|
|
290
|
+
return await plat.measure("Reconcile PIDs", async () => {
|
|
291
|
+
const result = new Map;
|
|
292
|
+
const needsReconciliation = processes.filter((p) => deadPids.has(p.pid) && p.pid > 0);
|
|
293
|
+
if (needsReconciliation.length === 0)
|
|
294
|
+
return result;
|
|
295
|
+
try {
|
|
296
|
+
let runningProcs = [];
|
|
297
|
+
if (isWindows()) {
|
|
298
|
+
const output = await psExec(`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | ForEach-Object { Write-Output "$($_.ProcessId)|$($_.CommandLine)" }`, 5000);
|
|
299
|
+
for (const line of output.split(`
|
|
300
|
+
`)) {
|
|
301
|
+
const sepIdx = line.indexOf("|");
|
|
302
|
+
if (sepIdx === -1)
|
|
303
|
+
continue;
|
|
304
|
+
const pid = parseInt(line.substring(0, sepIdx).trim());
|
|
305
|
+
const cmdLine = line.substring(sepIdx + 1).trim();
|
|
306
|
+
if (!isNaN(pid) && pid > 0 && cmdLine) {
|
|
307
|
+
runningProcs.push({ pid, cmdLine });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
const psOutput = await $`ps -eo pid,args --no-headers`.nothrow().quiet().text();
|
|
312
|
+
for (const line of psOutput.trim().split(`
|
|
313
|
+
`)) {
|
|
314
|
+
const match = line.trim().match(/^(\d+)\s+(.+)/);
|
|
315
|
+
if (match) {
|
|
316
|
+
runningProcs.push({ pid: parseInt(match[1]), cmdLine: match[2] });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
for (const proc of needsReconciliation) {
|
|
321
|
+
const cmdParts = proc.command.split(/\s+/);
|
|
322
|
+
const workdirParts = proc.workdir.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
323
|
+
const workdirLast = workdirParts[workdirParts.length - 1]?.toLowerCase() || "";
|
|
324
|
+
let bestMatch = null;
|
|
325
|
+
let ambiguous = false;
|
|
326
|
+
for (const running of runningProcs) {
|
|
327
|
+
const cmdLower = running.cmdLine.toLowerCase();
|
|
328
|
+
let score = 0;
|
|
329
|
+
for (const part of cmdParts) {
|
|
330
|
+
if (part.length > 2 && cmdLower.includes(part.toLowerCase()))
|
|
331
|
+
score++;
|
|
332
|
+
}
|
|
333
|
+
if (workdirLast && cmdLower.includes(workdirLast))
|
|
334
|
+
score += 3;
|
|
335
|
+
if (cmdLower.includes(proc.workdir.toLowerCase().replace(/\\/g, "/")))
|
|
336
|
+
score += 5;
|
|
337
|
+
if (cmdLower.includes(proc.workdir.toLowerCase()))
|
|
338
|
+
score += 5;
|
|
339
|
+
if (score < 4)
|
|
340
|
+
continue;
|
|
341
|
+
if (!bestMatch || score > bestMatch.score) {
|
|
342
|
+
ambiguous = false;
|
|
343
|
+
bestMatch = { pid: running.pid, score };
|
|
344
|
+
} else if (score === bestMatch.score) {
|
|
345
|
+
ambiguous = true;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (bestMatch && !ambiguous) {
|
|
349
|
+
result.set(proc.name, bestMatch.pid);
|
|
350
|
+
runningProcs = runningProcs.filter((p) => p.pid !== bestMatch.pid);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
} catch {}
|
|
354
|
+
return result;
|
|
355
|
+
}) ?? new Map;
|
|
356
|
+
}
|
|
357
|
+
async function findPidByPort(port, maxWaitMs = 8000) {
|
|
358
|
+
const start = Date.now();
|
|
359
|
+
const pollMs = 500;
|
|
360
|
+
while (Date.now() - start < maxWaitMs) {
|
|
361
|
+
try {
|
|
362
|
+
if (isWindows()) {
|
|
363
|
+
const result = await $`netstat -ano`.nothrow().quiet().text();
|
|
364
|
+
for (const line of result.split(`
|
|
365
|
+
`)) {
|
|
366
|
+
if (line.includes(`:${port}`) && line.includes("LISTENING")) {
|
|
367
|
+
const parts = line.trim().split(/\s+/);
|
|
368
|
+
const pid = parseInt(parts[parts.length - 1]);
|
|
369
|
+
if (!isNaN(pid) && pid > 0)
|
|
370
|
+
return pid;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
try {
|
|
375
|
+
const result2 = await $`ss -tlnp`.nothrow().quiet().text();
|
|
376
|
+
for (const line of result2.split(`
|
|
377
|
+
`)) {
|
|
378
|
+
if (line.includes(`:${port}`)) {
|
|
379
|
+
const pidMatch = line.match(/pid=(\d+)/);
|
|
380
|
+
if (pidMatch)
|
|
381
|
+
return parseInt(pidMatch[1]);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
} catch {}
|
|
385
|
+
const result = await $`lsof -iTCP:${port} -sTCP:LISTEN -t`.nothrow().quiet().text();
|
|
386
|
+
const pid = parseInt(result.trim());
|
|
387
|
+
if (!isNaN(pid) && pid > 0)
|
|
388
|
+
return pid;
|
|
389
|
+
}
|
|
390
|
+
} catch {}
|
|
391
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
214
395
|
async function readFileTail(filePath, lines) {
|
|
215
396
|
return await plat.measure(`Read tail ${lines ?? "all"}L`, async () => {
|
|
216
397
|
try {
|
|
@@ -227,6 +408,13 @@ async function readFileTail(filePath, lines) {
|
|
|
227
408
|
}
|
|
228
409
|
}) ?? "";
|
|
229
410
|
}
|
|
411
|
+
function copyFile(src, dest) {
|
|
412
|
+
fs.copyFileSync(src, dest);
|
|
413
|
+
}
|
|
414
|
+
async function getProcessMemory(pid) {
|
|
415
|
+
const map = await getProcessBatchResources([pid]);
|
|
416
|
+
return map.get(pid)?.memory || 0;
|
|
417
|
+
}
|
|
230
418
|
async function getProcessBatchResources(pids) {
|
|
231
419
|
if (pids.length === 0)
|
|
232
420
|
return new Map;
|
|
@@ -235,28 +423,16 @@ async function getProcessBatchResources(pids) {
|
|
|
235
423
|
const pidSet = new Set(pids);
|
|
236
424
|
try {
|
|
237
425
|
if (isWindows()) {
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
`)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (!trimmed || trimmed.startsWith("Id") || trimmed.startsWith("--"))
|
|
426
|
+
const output = psExec(`Get-Process -Id ${pids.join(",")} -ErrorAction SilentlyContinue | Select-Object Id, WorkingSet64 | ForEach-Object { Write-Output "$($_.Id)|$($_.WorkingSet64)" }`);
|
|
427
|
+
for (const line of output.split(`
|
|
428
|
+
`)) {
|
|
429
|
+
const sepIdx = line.indexOf("|");
|
|
430
|
+
if (sepIdx === -1)
|
|
244
431
|
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
|
-
}
|
|
432
|
+
const pid = parseInt(line.substring(0, sepIdx).trim());
|
|
433
|
+
const memory = parseInt(line.substring(sepIdx + 1).trim()) || 0;
|
|
434
|
+
if (!isNaN(pid) && pidSet.has(pid)) {
|
|
435
|
+
resourceMap.set(pid, { memory, cpu: 0 });
|
|
260
436
|
}
|
|
261
437
|
}
|
|
262
438
|
} else {
|
|
@@ -333,7 +509,6 @@ var init_platform = __esm(() => {
|
|
|
333
509
|
|
|
334
510
|
// src/utils.ts
|
|
335
511
|
import * as fs2 from "fs";
|
|
336
|
-
import chalk from "chalk";
|
|
337
512
|
function parseEnvString(envString) {
|
|
338
513
|
const env = {};
|
|
339
514
|
envString.split(",").forEach((pair) => {
|
|
@@ -351,8 +526,8 @@ function calculateRuntime(startTime) {
|
|
|
351
526
|
}
|
|
352
527
|
async function getVersion() {
|
|
353
528
|
try {
|
|
354
|
-
const { join } = await import("path");
|
|
355
|
-
const pkgPath =
|
|
529
|
+
const { join: join2 } = await import("path");
|
|
530
|
+
const pkgPath = join2(import.meta.dir, "../package.json");
|
|
356
531
|
const pkg = await Bun.file(pkgPath).json();
|
|
357
532
|
return pkg.version || "0.0.0";
|
|
358
533
|
} catch {
|
|
@@ -361,8 +536,7 @@ async function getVersion() {
|
|
|
361
536
|
}
|
|
362
537
|
function validateDirectory(directory) {
|
|
363
538
|
if (!directory || !fs2.existsSync(directory)) {
|
|
364
|
-
|
|
365
|
-
process.exit(1);
|
|
539
|
+
throw new Error(`Directory not found or invalid: '${directory}'`);
|
|
366
540
|
}
|
|
367
541
|
}
|
|
368
542
|
function tailFile(path, prefix, colorFn, lines) {
|
|
@@ -417,21 +591,31 @@ var exports_db = {};
|
|
|
417
591
|
__export(exports_db, {
|
|
418
592
|
updateProcessPid: () => updateProcessPid,
|
|
419
593
|
updateProcessEnv: () => updateProcessEnv,
|
|
594
|
+
saveTemplate: () => saveTemplate,
|
|
420
595
|
retryDatabaseOperation: () => retryDatabaseOperation,
|
|
421
596
|
removeProcessByName: () => removeProcessByName,
|
|
422
597
|
removeProcess: () => removeProcess,
|
|
423
598
|
removeAllProcesses: () => removeAllProcesses,
|
|
424
599
|
insertProcess: () => insertProcess,
|
|
600
|
+
getTemplate: () => getTemplate,
|
|
601
|
+
getRecentHistory: () => getRecentHistory,
|
|
602
|
+
getProcessHistory: () => getProcessHistory,
|
|
425
603
|
getProcess: () => getProcess,
|
|
426
604
|
getDbInfo: () => getDbInfo,
|
|
605
|
+
getAllTemplates: () => getAllTemplates,
|
|
427
606
|
getAllProcesses: () => getAllProcesses,
|
|
607
|
+
deleteTemplate: () => deleteTemplate,
|
|
428
608
|
dbPath: () => dbPath,
|
|
429
609
|
db: () => db,
|
|
610
|
+
clearOldHistory: () => clearOldHistory,
|
|
430
611
|
bgrHome: () => bgrHome,
|
|
431
|
-
|
|
612
|
+
addHistoryEntry: () => addHistoryEntry,
|
|
613
|
+
TemplateSchema: () => TemplateSchema,
|
|
614
|
+
ProcessSchema: () => ProcessSchema,
|
|
615
|
+
HistorySchema: () => HistorySchema
|
|
432
616
|
});
|
|
433
617
|
import { Database, z } from "sqlite-zod-orm";
|
|
434
|
-
import { join } from "path";
|
|
618
|
+
import { join as join2 } from "path";
|
|
435
619
|
var {sleep } = globalThis.Bun;
|
|
436
620
|
import { existsSync as existsSync3, copyFileSync as copyFileSync2 } from "fs";
|
|
437
621
|
function getProcess(name) {
|
|
@@ -476,6 +660,61 @@ function updateProcessEnv(name, envJson) {
|
|
|
476
660
|
db.process.update(proc.id, { env: envJson });
|
|
477
661
|
}
|
|
478
662
|
}
|
|
663
|
+
function getAllTemplates() {
|
|
664
|
+
return db.template.select().all();
|
|
665
|
+
}
|
|
666
|
+
function getTemplate(name) {
|
|
667
|
+
return db.template.select().where({ name }).limit(1).get() || null;
|
|
668
|
+
}
|
|
669
|
+
function saveTemplate(data) {
|
|
670
|
+
const existing = db.template.select().where({ name: data.name }).limit(1).get();
|
|
671
|
+
if (existing) {
|
|
672
|
+
db.template.update(existing.id, {
|
|
673
|
+
command: data.command,
|
|
674
|
+
workdir: data.workdir || "",
|
|
675
|
+
env: data.env || "",
|
|
676
|
+
group: data.group || ""
|
|
677
|
+
});
|
|
678
|
+
} else {
|
|
679
|
+
db.template.insert({
|
|
680
|
+
name: data.name,
|
|
681
|
+
command: data.command,
|
|
682
|
+
workdir: data.workdir || "",
|
|
683
|
+
env: data.env || "",
|
|
684
|
+
group: data.group || ""
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
function deleteTemplate(name) {
|
|
689
|
+
const tmpl = db.template.select().where({ name }).limit(1).get();
|
|
690
|
+
if (tmpl) {
|
|
691
|
+
db.template.delete(tmpl.id);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
function getProcessHistory(name, limit = 50) {
|
|
695
|
+
return db.history.select().where({ process_name: name }).orderBy("timestamp", "desc").limit(limit).all();
|
|
696
|
+
}
|
|
697
|
+
function addHistoryEntry(processName, event, pid, metadata = {}) {
|
|
698
|
+
return db.history.insert({
|
|
699
|
+
process_name: processName,
|
|
700
|
+
event,
|
|
701
|
+
pid,
|
|
702
|
+
metadata: JSON.stringify(metadata)
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
function getRecentHistory(limit = 100) {
|
|
706
|
+
return db.history.select().orderBy("timestamp", "desc").limit(limit).all();
|
|
707
|
+
}
|
|
708
|
+
function clearOldHistory(daysToKeep = 30) {
|
|
709
|
+
const cutoff = new Date;
|
|
710
|
+
cutoff.setDate(cutoff.getDate() - daysToKeep);
|
|
711
|
+
const cutoffStr = cutoff.toISOString();
|
|
712
|
+
const oldEntries = db.history.select().where("timestamp", "<", cutoffStr).all();
|
|
713
|
+
for (const entry of oldEntries) {
|
|
714
|
+
db.history.delete(entry.id);
|
|
715
|
+
}
|
|
716
|
+
return oldEntries.length;
|
|
717
|
+
}
|
|
479
718
|
function getDbInfo() {
|
|
480
719
|
return {
|
|
481
720
|
dbPath,
|
|
@@ -498,7 +737,7 @@ async function retryDatabaseOperation(operation, maxRetries = 5, delay = 100) {
|
|
|
498
737
|
}
|
|
499
738
|
throw new Error("Max retries reached for database operation");
|
|
500
739
|
}
|
|
501
|
-
var ProcessSchema, homePath, bgrDir, dbFilename, dbPath, bgrHome, legacyDbPath, db;
|
|
740
|
+
var ProcessSchema, TemplateSchema, HistorySchema, homePath, bgrDir, dbFilename, dbPath, bgrHome, legacyDbPath, db;
|
|
502
741
|
var init_db = __esm(() => {
|
|
503
742
|
init_platform();
|
|
504
743
|
ProcessSchema = z.object({
|
|
@@ -510,15 +749,31 @@ var init_db = __esm(() => {
|
|
|
510
749
|
configPath: z.string().default(""),
|
|
511
750
|
stdout_path: z.string(),
|
|
512
751
|
stderr_path: z.string(),
|
|
513
|
-
timestamp: z.string().default(() => new Date().toISOString())
|
|
752
|
+
timestamp: z.string().default(() => new Date().toISOString()),
|
|
753
|
+
group: z.string().default("")
|
|
754
|
+
});
|
|
755
|
+
TemplateSchema = z.object({
|
|
756
|
+
name: z.string(),
|
|
757
|
+
command: z.string(),
|
|
758
|
+
workdir: z.string().default(""),
|
|
759
|
+
env: z.string().default(""),
|
|
760
|
+
group: z.string().default(""),
|
|
761
|
+
created_at: z.string().default(() => new Date().toISOString())
|
|
762
|
+
});
|
|
763
|
+
HistorySchema = z.object({
|
|
764
|
+
process_name: z.string(),
|
|
765
|
+
event: z.string(),
|
|
766
|
+
pid: z.number().optional(),
|
|
767
|
+
timestamp: z.string().default(() => new Date().toISOString()),
|
|
768
|
+
metadata: z.string().default("")
|
|
514
769
|
});
|
|
515
770
|
homePath = getHomeDir();
|
|
516
|
-
bgrDir =
|
|
771
|
+
bgrDir = join2(homePath, ".bgr");
|
|
517
772
|
ensureDir(bgrDir);
|
|
518
773
|
dbFilename = process.env.BGRUN_DB ?? "bgrun.sqlite";
|
|
519
|
-
dbPath =
|
|
774
|
+
dbPath = join2(bgrDir, dbFilename);
|
|
520
775
|
bgrHome = bgrDir;
|
|
521
|
-
legacyDbPath =
|
|
776
|
+
legacyDbPath = join2(bgrDir, "bgr_v2.sqlite");
|
|
522
777
|
if (!existsSync3(dbPath) && existsSync3(legacyDbPath)) {
|
|
523
778
|
try {
|
|
524
779
|
copyFileSync2(legacyDbPath, dbPath);
|
|
@@ -526,17 +781,21 @@ var init_db = __esm(() => {
|
|
|
526
781
|
} catch (e) {}
|
|
527
782
|
}
|
|
528
783
|
db = new Database(dbPath, {
|
|
529
|
-
process: ProcessSchema
|
|
784
|
+
process: ProcessSchema,
|
|
785
|
+
template: TemplateSchema,
|
|
786
|
+
history: HistorySchema
|
|
530
787
|
}, {
|
|
531
788
|
indexes: {
|
|
532
|
-
process: ["name", "timestamp", "pid"]
|
|
789
|
+
process: ["name", "timestamp", "pid"],
|
|
790
|
+
template: ["name"],
|
|
791
|
+
history: ["process_name", "timestamp"]
|
|
533
792
|
}
|
|
534
793
|
});
|
|
535
794
|
});
|
|
536
795
|
|
|
537
796
|
// src/logger.ts
|
|
538
797
|
import boxen from "boxen";
|
|
539
|
-
import
|
|
798
|
+
import chalk from "chalk";
|
|
540
799
|
function announce(message, title) {
|
|
541
800
|
console.log(boxen(message, {
|
|
542
801
|
padding: 1,
|
|
@@ -549,7 +808,7 @@ function announce(message, title) {
|
|
|
549
808
|
}
|
|
550
809
|
function error(message) {
|
|
551
810
|
const text = message instanceof Error ? message.stack || message.message : String(message);
|
|
552
|
-
console.error(boxen(
|
|
811
|
+
console.error(boxen(chalk.red(text), {
|
|
553
812
|
padding: 1,
|
|
554
813
|
margin: 1,
|
|
555
814
|
borderColor: "red",
|
|
@@ -557,9 +816,17 @@ function error(message) {
|
|
|
557
816
|
titleAlignment: "center",
|
|
558
817
|
borderStyle: "double"
|
|
559
818
|
}));
|
|
560
|
-
|
|
819
|
+
throw new BgrunError(text);
|
|
561
820
|
}
|
|
562
|
-
var
|
|
821
|
+
var BgrunError;
|
|
822
|
+
var init_logger = __esm(() => {
|
|
823
|
+
BgrunError = class BgrunError extends Error {
|
|
824
|
+
constructor(message) {
|
|
825
|
+
super(message);
|
|
826
|
+
this.name = "BgrunError";
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
});
|
|
563
830
|
|
|
564
831
|
// src/config.ts
|
|
565
832
|
function formatEnvKey(key) {
|
|
@@ -684,10 +951,10 @@ var init_deps = __esm(() => {
|
|
|
684
951
|
// src/commands/run.ts
|
|
685
952
|
var {$: $2 } = globalThis.Bun;
|
|
686
953
|
var {sleep: sleep2 } = globalThis.Bun;
|
|
687
|
-
import { join as
|
|
954
|
+
import { join as join3 } from "path";
|
|
688
955
|
import { createMeasure as createMeasure2 } from "measure-fn";
|
|
689
956
|
async function handleRun(options) {
|
|
690
|
-
const { command, directory, env, name, configPath, force, fetch, stdout, stderr } = options;
|
|
957
|
+
const { command, directory, env, name, configPath, force, fetch: fetch2, stdout, stderr } = options;
|
|
691
958
|
const existingProcess = name ? getProcess(name) : null;
|
|
692
959
|
if (name && existingProcess) {
|
|
693
960
|
const { getUnmetDeps: getUnmetDeps2 } = await Promise.resolve().then(() => (init_deps(), exports_deps));
|
|
@@ -708,7 +975,7 @@ async function handleRun(options) {
|
|
|
708
975
|
const finalDirectory2 = directory || existingProcess.workdir;
|
|
709
976
|
validateDirectory(finalDirectory2);
|
|
710
977
|
$2.cwd(finalDirectory2);
|
|
711
|
-
if (
|
|
978
|
+
if (fetch2) {
|
|
712
979
|
if (!__require("fs").existsSync(__require("path").join(finalDirectory2, ".git"))) {
|
|
713
980
|
error(`Cannot --fetch: '${finalDirectory2}' is not a Git repository.`);
|
|
714
981
|
}
|
|
@@ -758,9 +1025,15 @@ async function handleRun(options) {
|
|
|
758
1025
|
if (cmdToMatch) {
|
|
759
1026
|
await run.measure("Zombie sweep", async () => {
|
|
760
1027
|
try {
|
|
761
|
-
const
|
|
1028
|
+
const cmdKeyword = cmdToMatch.split(" ")[1] || cmdToMatch;
|
|
1029
|
+
const GENERIC_KEYWORDS = ["dev", "run", "start", "serve", "build", "test"];
|
|
1030
|
+
if (GENERIC_KEYWORDS.includes(cmdKeyword.toLowerCase())) {
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
const currentPid = process.pid;
|
|
1034
|
+
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
1035
|
const zombiePids = result.split(`
|
|
763
|
-
`).map((l) => parseInt(l.trim())).filter((n) => !isNaN(n) && n > 0);
|
|
1036
|
+
`).map((l) => parseInt(l.trim())).filter((n) => !isNaN(n) && n > 0 && n !== currentPid);
|
|
764
1037
|
for (const zPid of zombiePids) {
|
|
765
1038
|
await $2`taskkill /F /T /PID ${zPid}`.nothrow().quiet();
|
|
766
1039
|
}
|
|
@@ -781,6 +1054,9 @@ async function handleRun(options) {
|
|
|
781
1054
|
const finalCommand = command || existingProcess.command;
|
|
782
1055
|
const finalDirectory = directory || existingProcess?.workdir;
|
|
783
1056
|
let finalEnv = env || (existingProcess ? parseEnvString(existingProcess.env) : {});
|
|
1057
|
+
if (!("BGR_KEEP_ALIVE" in finalEnv)) {
|
|
1058
|
+
finalEnv.BGR_KEEP_ALIVE = "true";
|
|
1059
|
+
}
|
|
784
1060
|
let finalConfigPath;
|
|
785
1061
|
if (configPath !== undefined) {
|
|
786
1062
|
finalConfigPath = configPath;
|
|
@@ -790,7 +1066,7 @@ async function handleRun(options) {
|
|
|
790
1066
|
finalConfigPath = ".config.toml";
|
|
791
1067
|
}
|
|
792
1068
|
if (finalConfigPath) {
|
|
793
|
-
const fullConfigPath =
|
|
1069
|
+
const fullConfigPath = join3(finalDirectory, finalConfigPath);
|
|
794
1070
|
if (await Bun.file(fullConfigPath).exists()) {
|
|
795
1071
|
const configEnv = await run.measure(`Parse config "${finalConfigPath}"`, async () => {
|
|
796
1072
|
try {
|
|
@@ -808,9 +1084,9 @@ async function handleRun(options) {
|
|
|
808
1084
|
console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
|
|
809
1085
|
}
|
|
810
1086
|
}
|
|
811
|
-
const stdoutPath = stdout || existingProcess?.stdout_path ||
|
|
1087
|
+
const stdoutPath = stdout || existingProcess?.stdout_path || join3(homePath2, ".bgr", `${name}-out.txt`);
|
|
812
1088
|
Bun.write(stdoutPath, "");
|
|
813
|
-
const stderrPath = stderr || existingProcess?.stderr_path ||
|
|
1089
|
+
const stderrPath = stderr || existingProcess?.stderr_path || join3(homePath2, ".bgr", `${name}-err.txt`);
|
|
814
1090
|
Bun.write(stderrPath, "");
|
|
815
1091
|
const actualPid = await run.measure(`Spawn "${name}" \u2192 ${finalCommand}`, async () => {
|
|
816
1092
|
const newProcess = Bun.spawn(getShellCommand(finalCommand), {
|
|
@@ -854,7 +1130,7 @@ __export(exports_log_rotation, {
|
|
|
854
1130
|
rotateLogFile: () => rotateLogFile,
|
|
855
1131
|
rotateAllLogs: () => rotateAllLogs
|
|
856
1132
|
});
|
|
857
|
-
import { existsSync as existsSync7, statSync as statSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
1133
|
+
import { existsSync as existsSync7, statSync as statSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
858
1134
|
function rotateLogFile(filePath, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
|
|
859
1135
|
try {
|
|
860
1136
|
if (!existsSync7(filePath))
|
|
@@ -870,7 +1146,7 @@ function rotateLogFile(filePath, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAU
|
|
|
870
1146
|
const truncated = lines.slice(-keepLines);
|
|
871
1147
|
const header = `--- [bgrun] Log rotated at ${new Date().toISOString()} (was ${lines.length} lines, ${formatBytes(stat.size)}) ---
|
|
872
1148
|
`;
|
|
873
|
-
|
|
1149
|
+
writeFileSync2(filePath, header + truncated.join(`
|
|
874
1150
|
`));
|
|
875
1151
|
return true;
|
|
876
1152
|
} catch {
|
|
@@ -922,22 +1198,87 @@ var init_log_rotation = __esm(() => {
|
|
|
922
1198
|
var exports_server = {};
|
|
923
1199
|
__export(exports_server, {
|
|
924
1200
|
startServer: () => startServer,
|
|
925
|
-
guardRestartCounts: () => guardRestartCounts
|
|
1201
|
+
guardRestartCounts: () => guardRestartCounts,
|
|
1202
|
+
guardEvents: () => guardEvents
|
|
926
1203
|
});
|
|
927
1204
|
import path2 from "path";
|
|
1205
|
+
async function cleanupPort(port) {
|
|
1206
|
+
if (process.platform !== "win32")
|
|
1207
|
+
return port;
|
|
1208
|
+
try {
|
|
1209
|
+
const proc = Bun.spawn([
|
|
1210
|
+
"powershell",
|
|
1211
|
+
"-NoProfile",
|
|
1212
|
+
"-Command",
|
|
1213
|
+
`Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
|
|
1214
|
+
], { stdout: "pipe", stderr: "pipe" });
|
|
1215
|
+
const text = await new Response(proc.stdout).text();
|
|
1216
|
+
const pid = parseInt(text.trim(), 10);
|
|
1217
|
+
if (!pid || pid === process.pid)
|
|
1218
|
+
return port;
|
|
1219
|
+
const checkProc = Bun.spawn([
|
|
1220
|
+
"powershell",
|
|
1221
|
+
"-NoProfile",
|
|
1222
|
+
"-Command",
|
|
1223
|
+
`Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`
|
|
1224
|
+
], { stdout: "pipe", stderr: "pipe" });
|
|
1225
|
+
const checkText = await new Response(checkProc.stdout).text();
|
|
1226
|
+
if (checkText.trim()) {
|
|
1227
|
+
console.log(`[server] Killing PID ${pid} holding port ${port}`);
|
|
1228
|
+
Bun.spawn(["taskkill", "/F", "/PID", String(pid)], { stdout: "pipe", stderr: "pipe" });
|
|
1229
|
+
await Bun.sleep(1000);
|
|
1230
|
+
return port;
|
|
1231
|
+
} else {
|
|
1232
|
+
const fallback = port + 1;
|
|
1233
|
+
console.log(`[server] \u26A0 Port ${port} held by zombie PID ${pid} \u2014 falling back to port ${fallback}`);
|
|
1234
|
+
return fallback;
|
|
1235
|
+
}
|
|
1236
|
+
} catch {
|
|
1237
|
+
return port;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
928
1240
|
async function startServer() {
|
|
929
1241
|
const { start } = await import("melina");
|
|
930
1242
|
const appDir = path2.join(import.meta.dir, "../dashboard/app");
|
|
931
|
-
const
|
|
1243
|
+
const requestedPort = process.env.BUN_PORT ? parseInt(process.env.BUN_PORT, 10) : 3000;
|
|
1244
|
+
_originalPort = requestedPort;
|
|
1245
|
+
const resolvedPort = await cleanupPort(requestedPort);
|
|
1246
|
+
_currentPort = resolvedPort;
|
|
1247
|
+
const needsExplicitPort = process.env.BUN_PORT || resolvedPort !== requestedPort;
|
|
932
1248
|
await start({
|
|
933
1249
|
appDir,
|
|
934
1250
|
defaultTitle: "bgrun Dashboard - Process Manager",
|
|
935
1251
|
globalCss: path2.join(appDir, "globals.css"),
|
|
936
|
-
...
|
|
1252
|
+
...needsExplicitPort && { port: resolvedPort }
|
|
937
1253
|
});
|
|
938
1254
|
startGuard();
|
|
939
1255
|
const { startLogRotation: startLogRotation2 } = await Promise.resolve().then(() => (init_log_rotation(), exports_log_rotation));
|
|
940
1256
|
startLogRotation2(() => getAllProcesses());
|
|
1257
|
+
if (resolvedPort !== requestedPort) {
|
|
1258
|
+
startStickyPortChecker();
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
function startStickyPortChecker() {
|
|
1262
|
+
const CHECK_INTERVAL_MS = 60000;
|
|
1263
|
+
console.log(`[server] Starting sticky port checker (original: ${_originalPort}, current: ${_currentPort})`);
|
|
1264
|
+
setInterval(async () => {
|
|
1265
|
+
if (_currentPort === _originalPort)
|
|
1266
|
+
return;
|
|
1267
|
+
try {
|
|
1268
|
+
const proc = Bun.spawn([
|
|
1269
|
+
"powershell",
|
|
1270
|
+
"-NoProfile",
|
|
1271
|
+
"-Command",
|
|
1272
|
+
`Get-NetTCPConnection -LocalPort ${_originalPort} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
|
|
1273
|
+
], { stdout: "pipe", stderr: "pipe" });
|
|
1274
|
+
const text = await new Response(proc.stdout).text();
|
|
1275
|
+
const pid = parseInt(text.trim(), 10);
|
|
1276
|
+
if (!pid) {
|
|
1277
|
+
console.log(`[server] \u2713 Original port ${_originalPort} is now available! Consider restarting to reclaim it.`);
|
|
1278
|
+
_currentPort = _originalPort;
|
|
1279
|
+
}
|
|
1280
|
+
} catch {}
|
|
1281
|
+
}, CHECK_INTERVAL_MS);
|
|
941
1282
|
}
|
|
942
1283
|
function startGuard() {
|
|
943
1284
|
console.log(`[guard] \u2713 Built-in process guard started (checking every ${GUARD_INTERVAL_MS / 1000}s)`);
|
|
@@ -959,6 +1300,7 @@ function startGuard() {
|
|
|
959
1300
|
if (now < nextRestart)
|
|
960
1301
|
continue;
|
|
961
1302
|
console.log(`[guard] \u26A0 Guarded process "${proc.name}" (PID ${proc.pid}) is dead, restarting...`);
|
|
1303
|
+
let success = false;
|
|
962
1304
|
try {
|
|
963
1305
|
await handleRun({
|
|
964
1306
|
action: "run",
|
|
@@ -966,9 +1308,16 @@ function startGuard() {
|
|
|
966
1308
|
force: true,
|
|
967
1309
|
remoteName: ""
|
|
968
1310
|
});
|
|
1311
|
+
success = true;
|
|
969
1312
|
const prevCount = guardRestartCounts.get(proc.name) || 0;
|
|
970
1313
|
const newCount = prevCount + 1;
|
|
971
1314
|
guardRestartCounts.set(proc.name, newCount);
|
|
1315
|
+
try {
|
|
1316
|
+
addHistoryEntry(proc.name, "restart", undefined, { by: "guard", count: newCount });
|
|
1317
|
+
} catch {}
|
|
1318
|
+
guardEvents.unshift({ time: now, name: proc.name, action: "restart", success: true });
|
|
1319
|
+
if (guardEvents.length > 100)
|
|
1320
|
+
guardEvents.pop();
|
|
972
1321
|
if (newCount > 5) {
|
|
973
1322
|
const backoffSeconds = Math.min(30 * Math.pow(2, newCount - 6), 300);
|
|
974
1323
|
guardNextRestartTime.set(proc.name, Date.now() + backoffSeconds * 1000);
|
|
@@ -978,6 +1327,9 @@ function startGuard() {
|
|
|
978
1327
|
}
|
|
979
1328
|
} catch (err) {
|
|
980
1329
|
console.error(`[guard] \u2717 Failed to restart "${proc.name}": ${err.message}`);
|
|
1330
|
+
guardEvents.unshift({ time: now, name: proc.name, action: "restart", success: false });
|
|
1331
|
+
if (guardEvents.length > 100)
|
|
1332
|
+
guardEvents.pop();
|
|
981
1333
|
}
|
|
982
1334
|
} else {
|
|
983
1335
|
const prevCount = guardRestartCounts.get(proc.name) || 0;
|
|
@@ -995,7 +1347,7 @@ function startGuard() {
|
|
|
995
1347
|
}
|
|
996
1348
|
}, GUARD_INTERVAL_MS);
|
|
997
1349
|
}
|
|
998
|
-
var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES, _g, guardRestartCounts, guardNextRestartTime;
|
|
1350
|
+
var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES, _g, guardRestartCounts, guardNextRestartTime, guardEvents, _originalPort = 3000, _currentPort = 3000;
|
|
999
1351
|
var init_server = __esm(() => {
|
|
1000
1352
|
init_db();
|
|
1001
1353
|
init_platform();
|
|
@@ -1007,8 +1359,11 @@ var init_server = __esm(() => {
|
|
|
1007
1359
|
_g.__bgrGuardRestartCounts = new Map;
|
|
1008
1360
|
if (!_g.__bgrGuardNextRestartTime)
|
|
1009
1361
|
_g.__bgrGuardNextRestartTime = new Map;
|
|
1362
|
+
if (!_g.__bgrGuardEvents)
|
|
1363
|
+
_g.__bgrGuardEvents = [];
|
|
1010
1364
|
guardRestartCounts = _g.__bgrGuardRestartCounts;
|
|
1011
1365
|
guardNextRestartTime = _g.__bgrGuardNextRestartTime;
|
|
1366
|
+
guardEvents = _g.__bgrGuardEvents;
|
|
1012
1367
|
});
|
|
1013
1368
|
|
|
1014
1369
|
// src/guard.ts
|
|
@@ -1016,6 +1371,38 @@ var exports_guard = {};
|
|
|
1016
1371
|
__export(exports_guard, {
|
|
1017
1372
|
startGuardLoop: () => startGuardLoop
|
|
1018
1373
|
});
|
|
1374
|
+
import { createHmac } from "crypto";
|
|
1375
|
+
async function notifyWebhook(event, name, details) {
|
|
1376
|
+
if (!WEBHOOK_URL)
|
|
1377
|
+
return;
|
|
1378
|
+
try {
|
|
1379
|
+
const payload = JSON.stringify({
|
|
1380
|
+
event,
|
|
1381
|
+
process: name,
|
|
1382
|
+
timestamp: new Date().toISOString(),
|
|
1383
|
+
...details
|
|
1384
|
+
});
|
|
1385
|
+
const headers = {
|
|
1386
|
+
"Content-Type": "application/json",
|
|
1387
|
+
"User-Agent": "bgrun-guard/1.0"
|
|
1388
|
+
};
|
|
1389
|
+
if (WEBHOOK_SECRET) {
|
|
1390
|
+
const sig = createHmac("sha256", WEBHOOK_SECRET).update(payload).digest("hex");
|
|
1391
|
+
headers["X-BGR-Signature"] = `sha256=${sig}`;
|
|
1392
|
+
}
|
|
1393
|
+
const controller = new AbortController;
|
|
1394
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
1395
|
+
await fetch(WEBHOOK_URL, {
|
|
1396
|
+
method: "POST",
|
|
1397
|
+
headers,
|
|
1398
|
+
body: payload,
|
|
1399
|
+
signal: controller.signal
|
|
1400
|
+
});
|
|
1401
|
+
clearTimeout(timeout);
|
|
1402
|
+
} catch (err) {
|
|
1403
|
+
console.error(`[guard] Webhook failed: ${err.message}`);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1019
1406
|
async function restartProcess(name) {
|
|
1020
1407
|
try {
|
|
1021
1408
|
await handleRun({
|
|
@@ -1064,6 +1451,7 @@ async function guardCycle() {
|
|
|
1064
1451
|
continue;
|
|
1065
1452
|
}
|
|
1066
1453
|
console.log(`[guard] \u26A0 "${proc.name}" (PID ${proc.pid}) is dead \u2014 restarting...`);
|
|
1454
|
+
notifyWebhook("crash", proc.name, { pid: proc.pid, isDashboard });
|
|
1067
1455
|
const success = await restartProcess(proc.name);
|
|
1068
1456
|
if (success) {
|
|
1069
1457
|
const count = (state.restartCounts.get(proc.name) || 0) + 1;
|
|
@@ -1077,6 +1465,9 @@ async function guardCycle() {
|
|
|
1077
1465
|
console.log(`[guard] \u2713 Restarted "${proc.name}" (#${count})`);
|
|
1078
1466
|
}
|
|
1079
1467
|
restarted++;
|
|
1468
|
+
notifyWebhook("restart", proc.name, { pid: proc.pid, restartCount: count, backoffMs: backoff });
|
|
1469
|
+
} else {
|
|
1470
|
+
notifyWebhook("restart_failed", proc.name, { pid: proc.pid });
|
|
1080
1471
|
}
|
|
1081
1472
|
} else if (alive) {
|
|
1082
1473
|
const count = state.restartCounts.get(proc.name) || 0;
|
|
@@ -1111,17 +1502,20 @@ async function startGuardLoop(intervalMs = DEFAULT_INTERVAL_MS) {
|
|
|
1111
1502
|
console.log(`[guard] Crash backoff threshold: ${CRASH_THRESHOLD} restarts`);
|
|
1112
1503
|
console.log(`[guard] Stability window: ${STABILITY_WINDOW_MS / 1000}s`);
|
|
1113
1504
|
console.log(`[guard] Monitoring: BGR_KEEP_ALIVE=true + bgr-dashboard`);
|
|
1505
|
+
console.log(`[guard] Webhook: ${WEBHOOK_URL || "(none \u2014 set BGR_WEBHOOK_URL to enable)"}`);
|
|
1114
1506
|
console.log(`[guard] Started: ${new Date().toLocaleString()}`);
|
|
1115
1507
|
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
1508
|
await guardCycle();
|
|
1117
1509
|
setInterval(guardCycle, interval);
|
|
1118
1510
|
}
|
|
1119
|
-
var DEFAULT_INTERVAL_MS = 30000, MAX_BACKOFF_MS, CRASH_THRESHOLD = 5, STABILITY_WINDOW_MS = 120000, state;
|
|
1511
|
+
var WEBHOOK_URL, WEBHOOK_SECRET, DEFAULT_INTERVAL_MS = 30000, MAX_BACKOFF_MS, CRASH_THRESHOLD = 5, STABILITY_WINDOW_MS = 120000, state;
|
|
1120
1512
|
var init_guard = __esm(() => {
|
|
1121
1513
|
init_db();
|
|
1122
1514
|
init_platform();
|
|
1123
1515
|
init_run();
|
|
1124
1516
|
init_utils();
|
|
1517
|
+
WEBHOOK_URL = process.env.BGR_WEBHOOK_URL || "";
|
|
1518
|
+
WEBHOOK_SECRET = process.env.BGR_WEBHOOK_SECRET || "";
|
|
1125
1519
|
MAX_BACKOFF_MS = 5 * 60000;
|
|
1126
1520
|
state = {
|
|
1127
1521
|
restartCounts: new Map,
|
|
@@ -1136,10 +1530,10 @@ init_run();
|
|
|
1136
1530
|
import { parseArgs } from "util";
|
|
1137
1531
|
|
|
1138
1532
|
// src/commands/list.ts
|
|
1139
|
-
import
|
|
1533
|
+
import chalk3 from "chalk";
|
|
1140
1534
|
|
|
1141
1535
|
// src/table.ts
|
|
1142
|
-
import
|
|
1536
|
+
import chalk2 from "chalk";
|
|
1143
1537
|
function getTerminalWidth() {
|
|
1144
1538
|
return process.stdout.columns || 120;
|
|
1145
1539
|
}
|
|
@@ -1221,7 +1615,7 @@ function renderBorder(widths, padding, style) {
|
|
|
1221
1615
|
function renderHorizontalTable(rows, columns, options = {}) {
|
|
1222
1616
|
const { maxWidth = getTerminalWidth(), padding = 2, borderStyle = "rounded", showHeaders = true } = options;
|
|
1223
1617
|
if (rows.length === 0)
|
|
1224
|
-
return { table:
|
|
1618
|
+
return { table: chalk2.gray("No data to display"), truncatedIndices: [] };
|
|
1225
1619
|
const borderChars = {
|
|
1226
1620
|
rounded: ["\u256D", "\u252C", "\u256E", "\u2500", "\u2502", "\u251C", "\u253C", "\u2524", "\u2570", "\u2534", "\u256F"],
|
|
1227
1621
|
single: ["\u250C", "\u252C", "\u2510", "\u2500", "\u2502", "\u251C", "\u253C", "\u2524", "\u2514", "\u2534", "\u2518"],
|
|
@@ -1237,7 +1631,7 @@ function renderHorizontalTable(rows, columns, options = {}) {
|
|
|
1237
1631
|
if (borderStyle !== "none")
|
|
1238
1632
|
lines.push(renderBorder(widthArray, padding, [tl, tc, tr, h]));
|
|
1239
1633
|
if (showHeaders) {
|
|
1240
|
-
const headerCells = columns.map((col, i) =>
|
|
1634
|
+
const headerCells = columns.map((col, i) => chalk2.bold(truncateString(col.header, widthArray[i]).padEnd(widthArray[i])));
|
|
1241
1635
|
lines.push(`${v}${cellPadding}${headerCells.join(`${cellPadding}${v}${cellPadding}`)}${cellPadding}${v}`);
|
|
1242
1636
|
if (borderStyle !== "none")
|
|
1243
1637
|
lines.push(renderBorder(widthArray, padding, [ml, mc, mr, h]));
|
|
@@ -1266,10 +1660,10 @@ function renderVerticalTree(rows, columns) {
|
|
|
1266
1660
|
if (index > 0)
|
|
1267
1661
|
lines.push("");
|
|
1268
1662
|
const name = row.name ? `'${row.name}'` : `(ID: ${row.id})`;
|
|
1269
|
-
lines.push(
|
|
1663
|
+
lines.push(chalk2.cyan(`\u25B6 ${name}`));
|
|
1270
1664
|
columns.forEach((col) => {
|
|
1271
1665
|
const value = col.formatter ? col.formatter(row[col.key]) : String(row[col.key] || "");
|
|
1272
|
-
lines.push(` \u251C\u2500 ${
|
|
1666
|
+
lines.push(` \u251C\u2500 ${chalk2.gray(`${col.header}:`)} ${value}`);
|
|
1273
1667
|
});
|
|
1274
1668
|
});
|
|
1275
1669
|
return lines.join(`
|
|
@@ -1288,15 +1682,15 @@ function renderHybridTable(rows, columns, options = {}) {
|
|
|
1288
1682
|
}
|
|
1289
1683
|
function renderProcessTable(processes, options) {
|
|
1290
1684
|
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 === "-" ?
|
|
1685
|
+
{ key: "id", header: "ID", formatter: (id) => chalk2.blue(id) },
|
|
1686
|
+
{ key: "pid", header: "PID", formatter: (pid) => chalk2.yellow(pid) },
|
|
1687
|
+
{ key: "name", header: "Name", formatter: (name) => chalk2.cyan.bold(name) },
|
|
1688
|
+
{ key: "port", header: "Port", formatter: (port) => port === "-" ? chalk2.gray(port) : chalk2.hex("#FF6B6B")(port) },
|
|
1689
|
+
{ key: "memory", header: "Memory", formatter: (mem) => mem === "-" ? chalk2.gray(mem) : chalk2.hex("#4ECDC4")(mem) },
|
|
1296
1690
|
{ key: "command", header: "Command" },
|
|
1297
|
-
{ key: "workdir", header: "Directory", formatter: (dir) =>
|
|
1691
|
+
{ key: "workdir", header: "Directory", formatter: (dir) => chalk2.gray(dir), truncator: truncatePath },
|
|
1298
1692
|
{ key: "status", header: "Status" },
|
|
1299
|
-
{ key: "runtime", header: "Runtime", formatter: (runtime) =>
|
|
1693
|
+
{ key: "runtime", header: "Runtime", formatter: (runtime) => chalk2.magenta(runtime) }
|
|
1300
1694
|
];
|
|
1301
1695
|
return renderHybridTable(processes, columns, options);
|
|
1302
1696
|
}
|
|
@@ -1322,10 +1716,29 @@ async function showAll(opts) {
|
|
|
1322
1716
|
const envVars = parseEnvString(proc.env);
|
|
1323
1717
|
return envVars["BGR_GROUP"] === opts.filter;
|
|
1324
1718
|
});
|
|
1719
|
+
const deadPids = new Set;
|
|
1720
|
+
const aliveCache = new Map;
|
|
1721
|
+
for (const proc of filtered) {
|
|
1722
|
+
const alive = await isProcessRunning(proc.pid, proc.command);
|
|
1723
|
+
aliveCache.set(proc.pid, alive);
|
|
1724
|
+
if (!alive && proc.pid > 0)
|
|
1725
|
+
deadPids.add(proc.pid);
|
|
1726
|
+
}
|
|
1727
|
+
if (deadPids.size > 0) {
|
|
1728
|
+
const reconciled = await reconcileProcessPids(filtered.map((p) => ({ name: p.name, pid: p.pid, command: p.command, workdir: p.workdir })), deadPids);
|
|
1729
|
+
for (const [name, newPid] of reconciled) {
|
|
1730
|
+
updateProcessPid(name, newPid);
|
|
1731
|
+
const proc = filtered.find((p) => p.name === name);
|
|
1732
|
+
if (proc) {
|
|
1733
|
+
proc.pid = newPid;
|
|
1734
|
+
aliveCache.set(newPid, true);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1325
1738
|
if (opts?.json) {
|
|
1326
1739
|
const jsonData = [];
|
|
1327
1740
|
for (const proc of filtered) {
|
|
1328
|
-
const isRunning = await isProcessRunning(proc.pid, proc.command);
|
|
1741
|
+
const isRunning = aliveCache.get(proc.pid) ?? await isProcessRunning(proc.pid, proc.command);
|
|
1329
1742
|
const envVars = parseEnvString(proc.env);
|
|
1330
1743
|
const ports = isRunning ? await getProcessPorts(proc.pid) : [];
|
|
1331
1744
|
jsonData.push({
|
|
@@ -1343,7 +1756,7 @@ async function showAll(opts) {
|
|
|
1343
1756
|
const allPids = filtered.map((p) => p.pid);
|
|
1344
1757
|
const resourceMap = await getProcessBatchResources(allPids);
|
|
1345
1758
|
for (const proc of filtered) {
|
|
1346
|
-
const isRunning = await isProcessRunning(proc.pid, proc.command);
|
|
1759
|
+
const isRunning = aliveCache.get(proc.pid) ?? await isProcessRunning(proc.pid, proc.command);
|
|
1347
1760
|
const runtime = calculateRuntime(proc.timestamp);
|
|
1348
1761
|
const mem = isRunning ? resourceMap.get(proc.pid)?.memory || 0 : 0;
|
|
1349
1762
|
const ports = isRunning ? await getProcessPorts(proc.pid) : [];
|
|
@@ -1355,7 +1768,7 @@ async function showAll(opts) {
|
|
|
1355
1768
|
memory: formatMemory(mem),
|
|
1356
1769
|
command: proc.command,
|
|
1357
1770
|
workdir: proc.workdir,
|
|
1358
|
-
status: isRunning ?
|
|
1771
|
+
status: isRunning ? chalk3.green.bold("\u25CF Running") : chalk3.red.bold("\u25CB Stopped"),
|
|
1359
1772
|
runtime
|
|
1360
1773
|
});
|
|
1361
1774
|
}
|
|
@@ -1375,7 +1788,7 @@ async function showAll(opts) {
|
|
|
1375
1788
|
console.log(tableOutput);
|
|
1376
1789
|
const runningCount = tableData.filter((p) => p.status.includes("Running")).length;
|
|
1377
1790
|
const stoppedCount = tableData.filter((p) => p.status.includes("Stopped")).length;
|
|
1378
|
-
console.log(
|
|
1791
|
+
console.log(chalk3.cyan(`Total: ${tableData.length} processes (${chalk3.green(`${runningCount} running`)}, ${chalk3.red(`${stoppedCount} stopped`)})`));
|
|
1379
1792
|
}
|
|
1380
1793
|
|
|
1381
1794
|
// src/commands/cleanup.ts
|
|
@@ -1506,7 +1919,7 @@ init_utils();
|
|
|
1506
1919
|
init_run();
|
|
1507
1920
|
import * as fs4 from "fs";
|
|
1508
1921
|
import path from "path";
|
|
1509
|
-
import
|
|
1922
|
+
import chalk4 from "chalk";
|
|
1510
1923
|
async function handleWatch(options, logOptions) {
|
|
1511
1924
|
let currentProcess = null;
|
|
1512
1925
|
let isRestarting = false;
|
|
@@ -1517,7 +1930,7 @@ async function handleWatch(options, logOptions) {
|
|
|
1517
1930
|
const isDead = !await isProcessRunning(proc.pid);
|
|
1518
1931
|
if (!isDead)
|
|
1519
1932
|
return false;
|
|
1520
|
-
console.log(
|
|
1933
|
+
console.log(chalk4.yellow(`\uD83D\uDC80 Process '${options.name}' died immediately after ${reason}\u2014dumping logs:`));
|
|
1521
1934
|
const readAndDump = (path2, color, label) => {
|
|
1522
1935
|
try {
|
|
1523
1936
|
if (fs4.existsSync(path2)) {
|
|
@@ -1531,14 +1944,14 @@ ${color(content)}
|
|
|
1531
1944
|
}
|
|
1532
1945
|
}
|
|
1533
1946
|
} catch (err) {
|
|
1534
|
-
console.warn(
|
|
1947
|
+
console.warn(chalk4.gray(`Could not read ${label} log: ${err}`));
|
|
1535
1948
|
}
|
|
1536
1949
|
};
|
|
1537
1950
|
if (logOptions.logType === "both" || logOptions.logType === "stdout") {
|
|
1538
|
-
readAndDump(proc.stdout_path,
|
|
1951
|
+
readAndDump(proc.stdout_path, chalk4.white, "\uD83D\uDCC4 Stdout");
|
|
1539
1952
|
}
|
|
1540
1953
|
if (logOptions.logType === "both" || logOptions.logType === "stderr") {
|
|
1541
|
-
readAndDump(proc.stderr_path,
|
|
1954
|
+
readAndDump(proc.stderr_path, chalk4.red, "\uD83D\uDCC4 Stderr");
|
|
1542
1955
|
}
|
|
1543
1956
|
return true;
|
|
1544
1957
|
};
|
|
@@ -1579,29 +1992,29 @@ ${color(content)}
|
|
|
1579
1992
|
const stops = [];
|
|
1580
1993
|
if (!logOptions.showLogs || !currentProcess)
|
|
1581
1994
|
return stops;
|
|
1582
|
-
console.log(
|
|
1995
|
+
console.log(chalk4.gray(`
|
|
1583
1996
|
` + "\u2500".repeat(50) + `
|
|
1584
1997
|
`));
|
|
1585
1998
|
if (logOptions.logType === "both" || logOptions.logType === "stdout") {
|
|
1586
|
-
console.log(
|
|
1587
|
-
console.log(
|
|
1999
|
+
console.log(chalk4.green.bold(`\uD83D\uDCC4 Tailing stdout for ${options.name}:`));
|
|
2000
|
+
console.log(chalk4.gray("\u2550".repeat(50)));
|
|
1588
2001
|
try {
|
|
1589
2002
|
await waitForLogReady(currentProcess.stdout_path);
|
|
1590
2003
|
} catch (err) {
|
|
1591
|
-
console.warn(
|
|
2004
|
+
console.warn(chalk4.yellow(`\u26A0\uFE0F Stdout log not ready yet for ${options.name}\u2014starting tail anyway: ${err.message}`));
|
|
1592
2005
|
}
|
|
1593
|
-
const stop = tailFile(currentProcess.stdout_path, "",
|
|
2006
|
+
const stop = tailFile(currentProcess.stdout_path, "", chalk4.white, logOptions.lines);
|
|
1594
2007
|
stops.push(stop);
|
|
1595
2008
|
}
|
|
1596
2009
|
if (logOptions.logType === "both" || logOptions.logType === "stderr") {
|
|
1597
|
-
console.log(
|
|
1598
|
-
console.log(
|
|
2010
|
+
console.log(chalk4.red.bold(`\uD83D\uDCC4 Tailing stderr for ${options.name}:`));
|
|
2011
|
+
console.log(chalk4.gray("\u2550".repeat(50)));
|
|
1599
2012
|
try {
|
|
1600
2013
|
await waitForLogReady(currentProcess.stderr_path);
|
|
1601
2014
|
} catch (err) {
|
|
1602
|
-
console.warn(
|
|
2015
|
+
console.warn(chalk4.yellow(`\u26A0\uFE0F Stderr log not ready yet for ${options.name}\u2014starting tail anyway: ${err.message}`));
|
|
1603
2016
|
}
|
|
1604
|
-
const stop = tailFile(currentProcess.stderr_path, "",
|
|
2017
|
+
const stop = tailFile(currentProcess.stderr_path, "", chalk4.red, logOptions.lines);
|
|
1605
2018
|
stops.push(stop);
|
|
1606
2019
|
}
|
|
1607
2020
|
return stops;
|
|
@@ -1626,7 +2039,7 @@ ${color(content)}
|
|
|
1626
2039
|
const died = await dumpLogsIfDead(currentProcess, restartReason);
|
|
1627
2040
|
if (died) {
|
|
1628
2041
|
if (lastRestartPath) {
|
|
1629
|
-
console.log(
|
|
2042
|
+
console.log(chalk4.yellow(`\u26A0\uFE0F Compile error on change\u2014pausing restarts until manual fix.`));
|
|
1630
2043
|
return;
|
|
1631
2044
|
} else {
|
|
1632
2045
|
error(`Failed to start process '${options.name}'. Aborting watch mode.`);
|
|
@@ -1639,7 +2052,7 @@ ${color(content)}
|
|
|
1639
2052
|
} finally {
|
|
1640
2053
|
isRestarting = false;
|
|
1641
2054
|
if (currentProcess) {
|
|
1642
|
-
console.log(
|
|
2055
|
+
console.log(chalk4.cyan(`
|
|
1643
2056
|
\uD83D\uDC40 Watching for file changes in: ${currentProcess.workdir}`));
|
|
1644
2057
|
}
|
|
1645
2058
|
}
|
|
@@ -1659,7 +2072,7 @@ ${color(content)}
|
|
|
1659
2072
|
}
|
|
1660
2073
|
tailStops = await startTails();
|
|
1661
2074
|
const workdir = currentProcess.workdir;
|
|
1662
|
-
console.log(
|
|
2075
|
+
console.log(chalk4.cyan(`
|
|
1663
2076
|
\uD83D\uDC40 Watching for file changes in: ${workdir}`));
|
|
1664
2077
|
const watcher = fs4.watch(workdir, { recursive: true }, (eventType, filename) => {
|
|
1665
2078
|
if (filename == null)
|
|
@@ -1672,7 +2085,7 @@ ${color(content)}
|
|
|
1672
2085
|
debounceTimeout = setTimeout(() => restartProcess(fullPath), 500);
|
|
1673
2086
|
});
|
|
1674
2087
|
const cleanup = async () => {
|
|
1675
|
-
console.log(
|
|
2088
|
+
console.log(chalk4.magenta(`
|
|
1676
2089
|
SIGINT received...`));
|
|
1677
2090
|
watcher.close();
|
|
1678
2091
|
tailStops.forEach((stop) => stop());
|
|
@@ -1695,7 +2108,7 @@ SIGINT received...`));
|
|
|
1695
2108
|
init_db();
|
|
1696
2109
|
init_logger();
|
|
1697
2110
|
init_platform();
|
|
1698
|
-
import
|
|
2111
|
+
import chalk5 from "chalk";
|
|
1699
2112
|
import * as fs5 from "fs";
|
|
1700
2113
|
async function showLogs(name, logType = "both", lines) {
|
|
1701
2114
|
const proc = getProcess(name);
|
|
@@ -1704,17 +2117,17 @@ async function showLogs(name, logType = "both", lines) {
|
|
|
1704
2117
|
return;
|
|
1705
2118
|
}
|
|
1706
2119
|
if (logType === "both" || logType === "stdout") {
|
|
1707
|
-
console.log(
|
|
1708
|
-
console.log(
|
|
2120
|
+
console.log(chalk5.green.bold(`\uD83D\uDCC4 Stdout logs for ${name}:`));
|
|
2121
|
+
console.log(chalk5.gray("\u2550".repeat(50)));
|
|
1709
2122
|
if (fs5.existsSync(proc.stdout_path)) {
|
|
1710
2123
|
try {
|
|
1711
2124
|
const output = await readFileTail(proc.stdout_path, lines);
|
|
1712
|
-
console.log(output ||
|
|
2125
|
+
console.log(output || chalk5.gray("(no output)"));
|
|
1713
2126
|
} catch (err) {
|
|
1714
|
-
console.log(
|
|
2127
|
+
console.log(chalk5.red(`Error reading stdout: ${err}`));
|
|
1715
2128
|
}
|
|
1716
2129
|
} else {
|
|
1717
|
-
console.log(
|
|
2130
|
+
console.log(chalk5.gray("(log file not found)"));
|
|
1718
2131
|
}
|
|
1719
2132
|
if (logType === "both") {
|
|
1720
2133
|
console.log(`
|
|
@@ -1722,17 +2135,17 @@ async function showLogs(name, logType = "both", lines) {
|
|
|
1722
2135
|
}
|
|
1723
2136
|
}
|
|
1724
2137
|
if (logType === "both" || logType === "stderr") {
|
|
1725
|
-
console.log(
|
|
1726
|
-
console.log(
|
|
2138
|
+
console.log(chalk5.red.bold(`\uD83D\uDCC4 Stderr logs for ${name}:`));
|
|
2139
|
+
console.log(chalk5.gray("\u2550".repeat(50)));
|
|
1727
2140
|
if (fs5.existsSync(proc.stderr_path)) {
|
|
1728
2141
|
try {
|
|
1729
2142
|
const output = await readFileTail(proc.stderr_path, lines);
|
|
1730
|
-
console.log(output ||
|
|
2143
|
+
console.log(output || chalk5.gray("(no errors)"));
|
|
1731
2144
|
} catch (err) {
|
|
1732
|
-
console.log(
|
|
2145
|
+
console.log(chalk5.red(`Error reading stderr: ${err}`));
|
|
1733
2146
|
}
|
|
1734
2147
|
} else {
|
|
1735
|
-
console.log(
|
|
2148
|
+
console.log(chalk5.gray("(log file not found)"));
|
|
1736
2149
|
}
|
|
1737
2150
|
}
|
|
1738
2151
|
}
|
|
@@ -1742,35 +2155,44 @@ init_logger();
|
|
|
1742
2155
|
init_db();
|
|
1743
2156
|
init_utils();
|
|
1744
2157
|
init_platform();
|
|
1745
|
-
import
|
|
2158
|
+
import chalk6 from "chalk";
|
|
1746
2159
|
async function showDetails(name) {
|
|
1747
2160
|
const proc = getProcess(name);
|
|
1748
2161
|
if (!proc) {
|
|
1749
2162
|
error(`No process found named '${name}'`);
|
|
1750
2163
|
return;
|
|
1751
2164
|
}
|
|
1752
|
-
|
|
2165
|
+
let isRunning = await isProcessRunning(proc.pid, proc.command);
|
|
2166
|
+
if (!isRunning && proc.pid > 0) {
|
|
2167
|
+
const reconciled = await reconcileProcessPids([{ name: proc.name, pid: proc.pid, command: proc.command, workdir: proc.workdir }], new Set([proc.pid]));
|
|
2168
|
+
const newPid = reconciled.get(proc.name);
|
|
2169
|
+
if (newPid) {
|
|
2170
|
+
updateProcessPid(proc.name, newPid);
|
|
2171
|
+
proc.pid = newPid;
|
|
2172
|
+
isRunning = true;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
1753
2175
|
const runtime = calculateRuntime(proc.timestamp);
|
|
1754
2176
|
const envVars = parseEnvString(proc.env);
|
|
1755
2177
|
const ports = isRunning ? await getProcessPorts(proc.pid) : [];
|
|
1756
|
-
const portDisplay = ports.length > 0 ? ports.map((p) =>
|
|
2178
|
+
const portDisplay = ports.length > 0 ? ports.map((p) => chalk6.hex("#FF6B6B")(`:${p}`)).join(", ") : null;
|
|
1757
2179
|
const details = `
|
|
1758
|
-
${
|
|
1759
|
-
${
|
|
1760
|
-
${
|
|
1761
|
-
${
|
|
1762
|
-
${
|
|
1763
|
-
${
|
|
1764
|
-
${
|
|
1765
|
-
${
|
|
1766
|
-
${
|
|
1767
|
-
${
|
|
1768
|
-
${
|
|
1769
|
-
${
|
|
2180
|
+
${chalk6.bold("Process Details:")}
|
|
2181
|
+
${chalk6.gray("\u2550".repeat(50))}
|
|
2182
|
+
${chalk6.cyan.bold("Name:")} ${proc.name}
|
|
2183
|
+
${chalk6.yellow.bold("PID:")} ${proc.pid}${portDisplay ? `
|
|
2184
|
+
${chalk6.hex("#FF6B6B").bold("Port:")} ${portDisplay}` : ""}
|
|
2185
|
+
${chalk6.bold("Status:")} ${isRunning ? chalk6.green.bold("\u25CF Running") : chalk6.red.bold("\u25CB Stopped")}
|
|
2186
|
+
${chalk6.magenta.bold("Runtime:")} ${runtime}
|
|
2187
|
+
${chalk6.blue.bold("Working Directory:")} ${proc.workdir}
|
|
2188
|
+
${chalk6.white.bold("Command:")} ${proc.command}
|
|
2189
|
+
${chalk6.gray.bold("Config Path:")} ${proc.configPath}
|
|
2190
|
+
${chalk6.green.bold("Stdout Path:")} ${proc.stdout_path}
|
|
2191
|
+
${chalk6.red.bold("Stderr Path:")} ${proc.stderr_path}
|
|
1770
2192
|
|
|
1771
|
-
${
|
|
1772
|
-
${
|
|
1773
|
-
${Object.entries(envVars).map(([key, value]) => `${
|
|
2193
|
+
${chalk6.bold("\uD83D\uDD27 Environment Variables:")}
|
|
2194
|
+
${chalk6.gray("\u2550".repeat(50))}
|
|
2195
|
+
${Object.entries(envVars).map(([key, value]) => `${chalk6.cyan.bold(key)} = ${chalk6.yellow(value)}`).join(`
|
|
1774
2196
|
`)}
|
|
1775
2197
|
`;
|
|
1776
2198
|
announce(details, `Process Details: ${name}`);
|
|
@@ -1781,8 +2203,8 @@ init_logger();
|
|
|
1781
2203
|
init_platform();
|
|
1782
2204
|
init_db();
|
|
1783
2205
|
import dedent from "dedent";
|
|
1784
|
-
import
|
|
1785
|
-
import { join as
|
|
2206
|
+
import chalk7 from "chalk";
|
|
2207
|
+
import { join as join4 } from "path";
|
|
1786
2208
|
var {sleep: sleep3 } = globalThis.Bun;
|
|
1787
2209
|
import { configure } from "measure-fn";
|
|
1788
2210
|
if (!Bun.argv.includes("--_serve")) {
|
|
@@ -1790,15 +2212,55 @@ if (!Bun.argv.includes("--_serve")) {
|
|
|
1790
2212
|
configure({ silent: true });
|
|
1791
2213
|
}
|
|
1792
2214
|
}
|
|
2215
|
+
function redirectConsoleToFiles() {
|
|
2216
|
+
const stdoutPath = Bun.env.BGR_STDOUT;
|
|
2217
|
+
const stderrPath = Bun.env.BGR_STDERR;
|
|
2218
|
+
if (!stdoutPath && !stderrPath)
|
|
2219
|
+
return;
|
|
2220
|
+
const { appendFileSync } = __require("fs");
|
|
2221
|
+
const stripAnsi2 = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
2222
|
+
const timestamp = () => new Date().toISOString().replace("T", " ").substring(0, 19);
|
|
2223
|
+
if (stdoutPath) {
|
|
2224
|
+
const origLog = console.log;
|
|
2225
|
+
const origWarn = console.warn;
|
|
2226
|
+
console.log = (...args) => {
|
|
2227
|
+
const line = `[${timestamp()}] ${stripAnsi2(args.map(String).join(" "))}
|
|
2228
|
+
`;
|
|
2229
|
+
try {
|
|
2230
|
+
appendFileSync(stdoutPath, line);
|
|
2231
|
+
} catch {}
|
|
2232
|
+
origLog.apply(console, args);
|
|
2233
|
+
};
|
|
2234
|
+
console.warn = (...args) => {
|
|
2235
|
+
const line = `[${timestamp()}] WARN: ${stripAnsi2(args.map(String).join(" "))}
|
|
2236
|
+
`;
|
|
2237
|
+
try {
|
|
2238
|
+
appendFileSync(stdoutPath, line);
|
|
2239
|
+
} catch {}
|
|
2240
|
+
origWarn.apply(console, args);
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
if (stderrPath) {
|
|
2244
|
+
const origError = console.error;
|
|
2245
|
+
console.error = (...args) => {
|
|
2246
|
+
const line = `[${timestamp()}] ERROR: ${stripAnsi2(args.map(String).join(" "))}
|
|
2247
|
+
`;
|
|
2248
|
+
try {
|
|
2249
|
+
appendFileSync(stderrPath, line);
|
|
2250
|
+
} catch {}
|
|
2251
|
+
origError.apply(console, args);
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
1793
2255
|
async function showHelp() {
|
|
1794
2256
|
const usage = dedent`
|
|
1795
|
-
${
|
|
1796
|
-
${
|
|
2257
|
+
${chalk7.bold("bgrun \u2014 Bun Background Runner")}
|
|
2258
|
+
${chalk7.gray("\u2550".repeat(50))}
|
|
1797
2259
|
|
|
1798
|
-
${
|
|
2260
|
+
${chalk7.yellow("Usage:")}
|
|
1799
2261
|
bgrun [name] [options]
|
|
1800
2262
|
|
|
1801
|
-
${
|
|
2263
|
+
${chalk7.yellow("Commands:")}
|
|
1802
2264
|
bgrun List all processes
|
|
1803
2265
|
bgrun [name] Show details for a process
|
|
1804
2266
|
bgrun --dashboard Launch web dashboard (managed by bgrun)
|
|
@@ -1811,7 +2273,7 @@ async function showHelp() {
|
|
|
1811
2273
|
bgrun --clean Remove all stopped processes
|
|
1812
2274
|
bgrun --nuke Delete ALL processes
|
|
1813
2275
|
|
|
1814
|
-
${
|
|
2276
|
+
${chalk7.yellow("Options:")}
|
|
1815
2277
|
--name <string> Process name (required for new)
|
|
1816
2278
|
--command <string> Process command (required for new)
|
|
1817
2279
|
--directory <path> Working directory (required for new)
|
|
@@ -1831,7 +2293,7 @@ async function showHelp() {
|
|
|
1831
2293
|
--port <number> Port for dashboard (default: 3000)
|
|
1832
2294
|
--help Show this help message
|
|
1833
2295
|
|
|
1834
|
-
${
|
|
2296
|
+
${chalk7.yellow("Examples:")}
|
|
1835
2297
|
bgrun --dashboard
|
|
1836
2298
|
bgrun --name myapp --command "bun run dev" --directory . --watch
|
|
1837
2299
|
bgrun myapp --logs --lines 50
|
|
@@ -1878,11 +2340,13 @@ async function run2() {
|
|
|
1878
2340
|
allowPositionals: true
|
|
1879
2341
|
});
|
|
1880
2342
|
if (values["_serve"]) {
|
|
2343
|
+
redirectConsoleToFiles();
|
|
1881
2344
|
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|
|
1882
2345
|
await startServer2();
|
|
1883
2346
|
return;
|
|
1884
2347
|
}
|
|
1885
2348
|
if (values["_guard-loop"]) {
|
|
2349
|
+
redirectConsoleToFiles();
|
|
1886
2350
|
const { startGuardLoop: startGuardLoop2 } = await Promise.resolve().then(() => (init_guard(), exports_guard));
|
|
1887
2351
|
const intervalStr = positionals[0];
|
|
1888
2352
|
const intervalMs = intervalStr ? parseInt(intervalStr) * 1000 : undefined;
|
|
@@ -1892,7 +2356,7 @@ async function run2() {
|
|
|
1892
2356
|
if (values.dashboard) {
|
|
1893
2357
|
const dashboardName = "bgr-dashboard";
|
|
1894
2358
|
const homePath3 = getHomeDir();
|
|
1895
|
-
const bgrDir2 =
|
|
2359
|
+
const bgrDir2 = join4(homePath3, ".bgr");
|
|
1896
2360
|
const requestedPort = values.port;
|
|
1897
2361
|
const existing = getProcess(dashboardName);
|
|
1898
2362
|
if (existing && await isProcessRunning(existing.pid)) {
|
|
@@ -1906,10 +2370,10 @@ async function run2() {
|
|
|
1906
2370
|
const portStr = existingPorts.length > 0 ? `:${existingPorts[0]}` : "(detecting...)";
|
|
1907
2371
|
announce(`Dashboard is already running (PID ${existing.pid})
|
|
1908
2372
|
|
|
1909
|
-
` + ` \uD83C\uDF10 ${
|
|
2373
|
+
` + ` \uD83C\uDF10 ${chalk7.cyan(`http://localhost${portStr}`)}
|
|
1910
2374
|
|
|
1911
|
-
Use ${
|
|
1912
|
-
Use ${
|
|
2375
|
+
Use ${chalk7.yellow(`bgrun --stop ${dashboardName}`)} to stop it
|
|
2376
|
+
Use ${chalk7.yellow(`bgrun --dashboard --force`)} to restart`, "BGR Dashboard");
|
|
1913
2377
|
return;
|
|
1914
2378
|
}
|
|
1915
2379
|
if (existing) {
|
|
@@ -1927,35 +2391,39 @@ async function run2() {
|
|
|
1927
2391
|
const scriptPath = resolve(process.argv[1]);
|
|
1928
2392
|
const spawnCommand = `bun run ${scriptPath} --_serve`;
|
|
1929
2393
|
const command = `bgrun --_serve`;
|
|
1930
|
-
const stdoutPath =
|
|
1931
|
-
const stderrPath =
|
|
2394
|
+
const stdoutPath = join4(bgrDir2, `${dashboardName}-out.txt`);
|
|
2395
|
+
const stderrPath = join4(bgrDir2, `${dashboardName}-err.txt`);
|
|
1932
2396
|
await Bun.write(stdoutPath, "");
|
|
1933
2397
|
await Bun.write(stderrPath, "");
|
|
1934
2398
|
const spawnEnv = { ...Bun.env };
|
|
1935
2399
|
if (requestedPort) {
|
|
1936
2400
|
spawnEnv.BUN_PORT = requestedPort;
|
|
1937
2401
|
}
|
|
2402
|
+
spawnEnv.BGR_STDOUT = stdoutPath;
|
|
2403
|
+
spawnEnv.BGR_STDERR = stderrPath;
|
|
1938
2404
|
const targetPort = parseInt(requestedPort || Bun.env.BUN_PORT || "3000");
|
|
1939
2405
|
if (!isNaN(targetPort) && targetPort > 0) {
|
|
1940
2406
|
const portFree = await isPortFree(targetPort);
|
|
1941
2407
|
if (!portFree) {
|
|
1942
|
-
console.log(
|
|
2408
|
+
console.log(chalk7.yellow(` \u26A1 Port ${targetPort} is occupied \u2014 reclaiming...`));
|
|
1943
2409
|
await killProcessOnPort(targetPort);
|
|
1944
2410
|
const freed = await waitForPortFree(targetPort, 5000);
|
|
1945
2411
|
if (!freed) {
|
|
1946
|
-
console.log(
|
|
2412
|
+
console.log(chalk7.red(` \u26A0 Could not free port ${targetPort} \u2014 dashboard may pick a fallback port`));
|
|
1947
2413
|
}
|
|
1948
2414
|
}
|
|
1949
2415
|
}
|
|
1950
2416
|
const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
|
|
1951
2417
|
env: spawnEnv,
|
|
1952
2418
|
cwd: bgrDir2,
|
|
1953
|
-
stdout:
|
|
1954
|
-
stderr:
|
|
2419
|
+
stdout: "ignore",
|
|
2420
|
+
stderr: "ignore",
|
|
2421
|
+
detached: true
|
|
1955
2422
|
});
|
|
1956
2423
|
newProcess.unref();
|
|
1957
2424
|
await sleep3(2000);
|
|
1958
|
-
const
|
|
2425
|
+
const resolvedPort = parseInt(requestedPort || Bun.env.BUN_PORT || "3000");
|
|
2426
|
+
const actualPid = await findPidByPort(resolvedPort, 1e4) ?? await findChildPid(newProcess.pid);
|
|
1959
2427
|
let actualPort = null;
|
|
1960
2428
|
for (let attempt = 0;attempt < 10; attempt++) {
|
|
1961
2429
|
const ports = await getProcessPorts(actualPid);
|
|
@@ -1978,19 +2446,19 @@ async function run2() {
|
|
|
1978
2446
|
const portDisplay = actualPort ? String(actualPort) : "(detecting...)";
|
|
1979
2447
|
const urlDisplay = actualPort ? `http://localhost:${actualPort}` : "http://localhost (port auto-assigned)";
|
|
1980
2448
|
const msg = dedent`
|
|
1981
|
-
${
|
|
1982
|
-
${
|
|
2449
|
+
${chalk7.bold("\u26A1 BGR Dashboard launched")}
|
|
2450
|
+
${chalk7.gray("\u2500".repeat(40))}
|
|
1983
2451
|
|
|
1984
|
-
\u{1f310} Open in browser: ${
|
|
2452
|
+
\u{1f310} Open in browser: ${chalk7.cyan.underline(urlDisplay)}
|
|
1985
2453
|
\u{1f4ca} Manage all your processes from the web UI
|
|
1986
2454
|
\u{1f504} Auto-refreshes every 3 seconds
|
|
1987
2455
|
|
|
1988
|
-
${
|
|
1989
|
-
Process: ${
|
|
2456
|
+
${chalk7.gray("\u2500".repeat(40))}
|
|
2457
|
+
Process: ${chalk7.white(dashboardName)} | PID: ${chalk7.white(String(actualPid))} | Port: ${chalk7.white(portDisplay)}
|
|
1990
2458
|
|
|
1991
|
-
${
|
|
1992
|
-
${
|
|
1993
|
-
${
|
|
2459
|
+
${chalk7.yellow("bgrun bgr-dashboard --logs")} View dashboard logs
|
|
2460
|
+
${chalk7.yellow("bgrun --stop bgr-dashboard")} Stop the dashboard
|
|
2461
|
+
${chalk7.yellow("bgrun --restart bgr-dashboard")} Restart the dashboard
|
|
1994
2462
|
`;
|
|
1995
2463
|
announce(msg, "BGR Dashboard");
|
|
1996
2464
|
return;
|
|
@@ -1998,13 +2466,13 @@ async function run2() {
|
|
|
1998
2466
|
if (values.guard) {
|
|
1999
2467
|
const guardName = "bgr-guard";
|
|
2000
2468
|
const homePath3 = getHomeDir();
|
|
2001
|
-
const bgrDir2 =
|
|
2469
|
+
const bgrDir2 = join4(homePath3, ".bgr");
|
|
2002
2470
|
const existing = getProcess(guardName);
|
|
2003
2471
|
if (existing && await isProcessRunning(existing.pid)) {
|
|
2004
2472
|
announce(`Guard is already running (PID ${existing.pid})
|
|
2005
2473
|
|
|
2006
|
-
Use ${
|
|
2007
|
-
Use ${
|
|
2474
|
+
Use ${chalk7.yellow(`bgrun --stop ${guardName}`)} to stop it
|
|
2475
|
+
Use ${chalk7.yellow(`bgrun --guard --force`)} to restart`, "BGR Guard");
|
|
2008
2476
|
return;
|
|
2009
2477
|
}
|
|
2010
2478
|
if (existing) {
|
|
@@ -2017,19 +2485,27 @@ async function run2() {
|
|
|
2017
2485
|
const scriptPath = resolve(process.argv[1]);
|
|
2018
2486
|
const spawnCommand = `bun run ${scriptPath} --_guard-loop`;
|
|
2019
2487
|
const command = `bgrun --_guard-loop`;
|
|
2020
|
-
const stdoutPath =
|
|
2021
|
-
const stderrPath =
|
|
2488
|
+
const stdoutPath = join4(bgrDir2, `${guardName}-out.txt`);
|
|
2489
|
+
const stderrPath = join4(bgrDir2, `${guardName}-err.txt`);
|
|
2022
2490
|
await Bun.write(stdoutPath, "");
|
|
2023
2491
|
await Bun.write(stderrPath, "");
|
|
2024
2492
|
const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
|
|
2025
|
-
env: { ...Bun.env },
|
|
2493
|
+
env: { ...Bun.env, BGR_STDOUT: stdoutPath, BGR_STDERR: stderrPath },
|
|
2026
2494
|
cwd: bgrDir2,
|
|
2027
|
-
stdout:
|
|
2028
|
-
stderr:
|
|
2495
|
+
stdout: "ignore",
|
|
2496
|
+
stderr: "ignore",
|
|
2497
|
+
detached: true
|
|
2029
2498
|
});
|
|
2030
2499
|
newProcess.unref();
|
|
2031
2500
|
await sleep3(1000);
|
|
2032
|
-
|
|
2501
|
+
let actualPid = await findChildPid(newProcess.pid);
|
|
2502
|
+
if (!await isProcessRunning(actualPid)) {
|
|
2503
|
+
const { psExec: ps } = await Promise.resolve().then(() => (init_platform(), exports_platform));
|
|
2504
|
+
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);
|
|
2505
|
+
const foundPid = parseInt(result.trim());
|
|
2506
|
+
if (!isNaN(foundPid) && foundPid > 0)
|
|
2507
|
+
actualPid = foundPid;
|
|
2508
|
+
}
|
|
2033
2509
|
await retryDatabaseOperation(() => insertProcess({
|
|
2034
2510
|
pid: actualPid,
|
|
2035
2511
|
workdir: bgrDir2,
|
|
@@ -2041,19 +2517,19 @@ async function run2() {
|
|
|
2041
2517
|
stderr_path: stderrPath
|
|
2042
2518
|
}));
|
|
2043
2519
|
const msg = dedent`
|
|
2044
|
-
${
|
|
2045
|
-
${
|
|
2520
|
+
${chalk7.bold("\uD83D\uDEE1\uFE0F BGR Standalone Guard launched")}
|
|
2521
|
+
${chalk7.gray("\u2500".repeat(40))}
|
|
2046
2522
|
|
|
2047
2523
|
Monitors: All processes with BGR_KEEP_ALIVE=true
|
|
2048
2524
|
Also watches: bgr-dashboard (auto-restart if it dies)
|
|
2049
2525
|
Check interval: 30 seconds
|
|
2050
2526
|
Backoff: Exponential after 5 rapid crashes
|
|
2051
2527
|
|
|
2052
|
-
${
|
|
2053
|
-
Process: ${
|
|
2528
|
+
${chalk7.gray("\u2500".repeat(40))}
|
|
2529
|
+
Process: ${chalk7.white(guardName)} | PID: ${chalk7.white(String(actualPid))}
|
|
2054
2530
|
|
|
2055
|
-
${
|
|
2056
|
-
${
|
|
2531
|
+
${chalk7.yellow(`bgrun ${guardName} --logs`)} View guard logs
|
|
2532
|
+
${chalk7.yellow(`bgrun --stop ${guardName}`)} Stop the guard
|
|
2057
2533
|
`;
|
|
2058
2534
|
announce(msg, "BGR Guard");
|
|
2059
2535
|
return;
|
|
@@ -2070,13 +2546,13 @@ async function run2() {
|
|
|
2070
2546
|
const info = getDbInfo();
|
|
2071
2547
|
const version = await getVersion();
|
|
2072
2548
|
console.log(dedent`
|
|
2073
|
-
${
|
|
2074
|
-
${
|
|
2075
|
-
Version: ${
|
|
2076
|
-
BGR Home: ${
|
|
2077
|
-
DB Path: ${
|
|
2549
|
+
${chalk7.bold("bgrun debug info")}
|
|
2550
|
+
${chalk7.gray("\u2500".repeat(40))}
|
|
2551
|
+
Version: ${chalk7.cyan(version)}
|
|
2552
|
+
BGR Home: ${chalk7.yellow(info.bgrHome)}
|
|
2553
|
+
DB Path: ${chalk7.yellow(info.dbPath)}
|
|
2078
2554
|
DB File: ${info.dbFilename}
|
|
2079
|
-
DB Exists: ${info.exists ?
|
|
2555
|
+
DB Exists: ${info.exists ? chalk7.green("\u2713") : chalk7.red("\u2717")}
|
|
2080
2556
|
Platform: ${process.platform}
|
|
2081
2557
|
Bun: ${Bun.version}
|
|
2082
2558
|
`);
|
|
@@ -2097,12 +2573,12 @@ async function run2() {
|
|
|
2097
2573
|
error("No processes registered.");
|
|
2098
2574
|
return;
|
|
2099
2575
|
}
|
|
2100
|
-
console.log(
|
|
2576
|
+
console.log(chalk7.bold(`
|
|
2101
2577
|
Restarting ${all.length} processes...
|
|
2102
2578
|
`));
|
|
2103
2579
|
for (const proc of all) {
|
|
2104
2580
|
try {
|
|
2105
|
-
console.log(
|
|
2581
|
+
console.log(chalk7.yellow(` \u21BB Restarting ${proc.name}...`));
|
|
2106
2582
|
await handleRun({
|
|
2107
2583
|
action: "run",
|
|
2108
2584
|
name: proc.name,
|
|
@@ -2110,10 +2586,10 @@ async function run2() {
|
|
|
2110
2586
|
remoteName: ""
|
|
2111
2587
|
});
|
|
2112
2588
|
} catch (err) {
|
|
2113
|
-
console.error(
|
|
2589
|
+
console.error(chalk7.red(` \u2717 Failed to restart ${proc.name}: ${err.message}`));
|
|
2114
2590
|
}
|
|
2115
2591
|
}
|
|
2116
|
-
console.log(
|
|
2592
|
+
console.log(chalk7.green(`
|
|
2117
2593
|
\u2713 All processes restarted.
|
|
2118
2594
|
`));
|
|
2119
2595
|
return;
|
|
@@ -2125,22 +2601,22 @@ async function run2() {
|
|
|
2125
2601
|
error("No processes registered.");
|
|
2126
2602
|
return;
|
|
2127
2603
|
}
|
|
2128
|
-
console.log(
|
|
2604
|
+
console.log(chalk7.bold(`
|
|
2129
2605
|
Stopping ${all.length} processes...
|
|
2130
2606
|
`));
|
|
2131
2607
|
for (const proc of all) {
|
|
2132
2608
|
try {
|
|
2133
2609
|
if (await isProcessRunning(proc.pid)) {
|
|
2134
|
-
console.log(
|
|
2610
|
+
console.log(chalk7.yellow(` \u25A0 Stopping ${proc.name} (PID ${proc.pid})...`));
|
|
2135
2611
|
await handleStop(proc.name);
|
|
2136
2612
|
} else {
|
|
2137
|
-
console.log(
|
|
2613
|
+
console.log(chalk7.gray(` \u25CB ${proc.name} already stopped`));
|
|
2138
2614
|
}
|
|
2139
2615
|
} catch (err) {
|
|
2140
|
-
console.error(
|
|
2616
|
+
console.error(chalk7.red(` \u2717 Failed to stop ${proc.name}: ${err.message}`));
|
|
2141
2617
|
}
|
|
2142
2618
|
}
|
|
2143
|
-
console.log(
|
|
2619
|
+
console.log(chalk7.green(`
|
|
2144
2620
|
\u2713 All processes stopped.
|
|
2145
2621
|
`));
|
|
2146
2622
|
return;
|
|
@@ -2230,5 +2706,8 @@ async function run2() {
|
|
|
2230
2706
|
}
|
|
2231
2707
|
}
|
|
2232
2708
|
run2().catch((err) => {
|
|
2233
|
-
|
|
2709
|
+
if (err.name !== "BgrunError") {
|
|
2710
|
+
console.error(err);
|
|
2711
|
+
}
|
|
2712
|
+
process.exit(1);
|
|
2234
2713
|
});
|