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/src/platform.ts
CHANGED
|
@@ -11,6 +11,28 @@ import { measure, createMeasure } from "measure-fn";
|
|
|
11
11
|
|
|
12
12
|
const plat = createMeasure('platform');
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Execute a PowerShell command with -NoProfile synchronously.
|
|
16
|
+
* Returns stdout as string, or empty string on error.
|
|
17
|
+
*
|
|
18
|
+
* Uses Bun.spawnSync to avoid async stream hanging issues on Windows
|
|
19
|
+
* where Bun's pipe reader hangs indefinitely when reading killed processes.
|
|
20
|
+
*/
|
|
21
|
+
export function psExec(command: string, _timeoutMs: number = 3000): string {
|
|
22
|
+
const tmpFile = join(os.tmpdir(), `bgr-ps-${Date.now()}.ps1`);
|
|
23
|
+
try {
|
|
24
|
+
fs.writeFileSync(tmpFile, command);
|
|
25
|
+
const result = Bun.spawnSync(
|
|
26
|
+
['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', tmpFile]
|
|
27
|
+
);
|
|
28
|
+
try { fs.unlinkSync(tmpFile); } catch { }
|
|
29
|
+
return result.stdout?.toString() || '';
|
|
30
|
+
} catch {
|
|
31
|
+
try { fs.unlinkSync(tmpFile); } catch { }
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
14
36
|
/** Detect if running on Windows - use function to prevent bundler tree-shaking */
|
|
15
37
|
export function isWindows(): boolean {
|
|
16
38
|
return process.platform === "win32";
|
|
@@ -39,8 +61,19 @@ export async function isProcessRunning(pid: number, command?: string): Promise<b
|
|
|
39
61
|
}
|
|
40
62
|
|
|
41
63
|
if (isWindows()) {
|
|
42
|
-
|
|
43
|
-
return
|
|
64
|
+
// Fast path: signal 0 works for many native Windows/Bun invocations.
|
|
65
|
+
// But under MSYS/Git Bash or detached wrapper scenarios it can return
|
|
66
|
+
// false negatives for live Windows PIDs. Fall back to Get-Process so
|
|
67
|
+
// CLI, dashboard, and guard all agree on process liveness.
|
|
68
|
+
try {
|
|
69
|
+
process.kill(pid, 0);
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
const output = psExec(
|
|
73
|
+
`Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`
|
|
74
|
+
).trim();
|
|
75
|
+
return output === String(pid);
|
|
76
|
+
}
|
|
44
77
|
} else {
|
|
45
78
|
const result = await $`ps -p ${pid}`.nothrow().text();
|
|
46
79
|
return result.includes(`${pid}`);
|
|
@@ -86,8 +119,10 @@ async function isDockerContainerRunning(command: string): Promise<boolean> {
|
|
|
86
119
|
async function getChildPids(pid: number): Promise<number[]> {
|
|
87
120
|
try {
|
|
88
121
|
if (isWindows()) {
|
|
89
|
-
|
|
90
|
-
|
|
122
|
+
const result = await psExec(
|
|
123
|
+
`Get-CimInstance Win32_Process -Filter 'ParentProcessId=${pid}' | Select-Object -ExpandProperty ProcessId`,
|
|
124
|
+
3000
|
|
125
|
+
);
|
|
91
126
|
return result
|
|
92
127
|
.split('\n')
|
|
93
128
|
.map(line => parseInt(line.trim()))
|
|
@@ -176,6 +211,39 @@ export async function isPortFree(port: number): Promise<boolean> {
|
|
|
176
211
|
}
|
|
177
212
|
}
|
|
178
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Get info about what's using a port.
|
|
216
|
+
* Returns { inUse: boolean, pid?: number, processName?: string }
|
|
217
|
+
*/
|
|
218
|
+
export async function getPortInfo(port: number): Promise<{ inUse: boolean; pid?: number; processName?: string }> {
|
|
219
|
+
try {
|
|
220
|
+
if (isWindows()) {
|
|
221
|
+
const result = await $`netstat -ano | findstr :${port}`.nothrow().quiet().text();
|
|
222
|
+
for (const line of result.split('\n')) {
|
|
223
|
+
const match = line.match(new RegExp(`:(${port})\\s+.*LISTENING\\s+(\\d+)`));
|
|
224
|
+
if (match) {
|
|
225
|
+
const pid = parseInt(match[2]);
|
|
226
|
+
if (pid > 0 && await isProcessRunning(pid)) {
|
|
227
|
+
// Get process name
|
|
228
|
+
const nameResult = await $`powershell -NoProfile -Command "(Get-Process -Id ${pid} -ErrorAction SilentlyContinue).ProcessName"`.nothrow().quiet().text();
|
|
229
|
+
return { inUse: true, pid, processName: nameResult.trim() || 'unknown' };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return { inUse: false };
|
|
234
|
+
} else {
|
|
235
|
+
const result = await $`ss -tln sport = :${port}`.nothrow().quiet().text();
|
|
236
|
+
const lines = result.trim().split('\n').filter((l: string) => l.trim());
|
|
237
|
+
if (lines.length > 1) {
|
|
238
|
+
return { inUse: true };
|
|
239
|
+
}
|
|
240
|
+
return { inUse: false };
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
return { inUse: false };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
179
247
|
/**
|
|
180
248
|
* Wait for a port to become free, polling with timeout.
|
|
181
249
|
* Returns true if port is free, false if timeout reached.
|
|
@@ -265,23 +333,26 @@ export function getShellCommand(command: string): string[] {
|
|
|
265
333
|
return ["sh", "-c", command];
|
|
266
334
|
}
|
|
267
335
|
}
|
|
268
|
-
|
|
269
336
|
/**
|
|
270
337
|
* Find the actual child process PID spawned by a shell wrapper.
|
|
271
|
-
* Traverses the process tree
|
|
272
|
-
* On Windows, bgr spawn creates: cmd.exe →
|
|
273
|
-
*
|
|
338
|
+
* Traverses the process tree to find the deepest (leaf) child.
|
|
339
|
+
* On Windows, bgr spawn creates: cmd.exe → bun.exe (typically 1-2 levels)
|
|
340
|
+
*
|
|
341
|
+
* Uses PowerShell with -NoProfile and a hard timeout to prevent hangs.
|
|
274
342
|
*/
|
|
275
343
|
export async function findChildPid(parentPid: number): Promise<number> {
|
|
276
344
|
let currentPid = parentPid;
|
|
277
|
-
const maxDepth =
|
|
345
|
+
const maxDepth = 2; // cmd.exe → bun.exe is the typical chain
|
|
278
346
|
|
|
279
347
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
280
348
|
try {
|
|
281
349
|
let childPids: number[] = [];
|
|
282
350
|
|
|
283
351
|
if (isWindows()) {
|
|
284
|
-
const result = await
|
|
352
|
+
const result = await psExec(
|
|
353
|
+
`Get-CimInstance Win32_Process -Filter 'ParentProcessId=${currentPid}' | Select-Object -ExpandProperty ProcessId`,
|
|
354
|
+
3000
|
|
355
|
+
);
|
|
285
356
|
childPids = result
|
|
286
357
|
.split('\n')
|
|
287
358
|
.map((line: string) => parseInt(line.trim()))
|
|
@@ -295,12 +366,7 @@ export async function findChildPid(parentPid: number): Promise<number> {
|
|
|
295
366
|
.filter(n => !isNaN(n) && n > 0);
|
|
296
367
|
}
|
|
297
368
|
|
|
298
|
-
if (childPids.length === 0)
|
|
299
|
-
// No children — currentPid is the leaf process
|
|
300
|
-
break;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Follow the first child deeper
|
|
369
|
+
if (childPids.length === 0) break;
|
|
304
370
|
currentPid = childPids[0];
|
|
305
371
|
} catch {
|
|
306
372
|
break;
|
|
@@ -335,14 +401,10 @@ export async function reconcileProcessPids(
|
|
|
335
401
|
let runningProcs: Array<{ pid: number; cmdLine: string }> = [];
|
|
336
402
|
|
|
337
403
|
if (isWindows()) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const ps = Bun.spawnSync(['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', tmpScript]);
|
|
344
|
-
const output = ps.stdout.toString();
|
|
345
|
-
|
|
404
|
+
const output = await psExec(
|
|
405
|
+
`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | ForEach-Object { Write-Output "$($_.ProcessId)|$($_.CommandLine)" }`,
|
|
406
|
+
5000
|
|
407
|
+
);
|
|
346
408
|
for (const line of output.split('\n')) {
|
|
347
409
|
const sepIdx = line.indexOf('|');
|
|
348
410
|
if (sepIdx === -1) continue;
|
|
@@ -508,32 +570,17 @@ export async function getProcessBatchResources(pids: number[]): Promise<Map<numb
|
|
|
508
570
|
|
|
509
571
|
try {
|
|
510
572
|
if (isWindows()) {
|
|
511
|
-
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
if (
|
|
521
|
-
|
|
522
|
-
// CPU can sometimes be blank if process is just starting, handle that
|
|
523
|
-
let cpuStr = parts[1];
|
|
524
|
-
let memStr = parts[2];
|
|
525
|
-
if (parts.length === 2) {
|
|
526
|
-
// If CPU is missing, powershell might omit it and give just ID and WorkingSet
|
|
527
|
-
cpuStr = "0";
|
|
528
|
-
memStr = parts[1];
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
const cpu = parseFloat(cpuStr) || 0;
|
|
532
|
-
const memory = parseInt(memStr) || 0;
|
|
533
|
-
|
|
534
|
-
if (!isNaN(pid) && !isNaN(memory)) {
|
|
535
|
-
if (pidSet.has(pid)) resourceMap.set(pid, { memory, cpu });
|
|
536
|
-
}
|
|
573
|
+
// psExec(Get-Process) is fast (~2ms) vs tasklist which hangs
|
|
574
|
+
const output = psExec(
|
|
575
|
+
`Get-Process -Id ${pids.join(',')} -ErrorAction SilentlyContinue | Select-Object Id, WorkingSet64 | ForEach-Object { Write-Output "$($_.Id)|$($_.WorkingSet64)" }`
|
|
576
|
+
);
|
|
577
|
+
for (const line of output.split('\n')) {
|
|
578
|
+
const sepIdx = line.indexOf('|');
|
|
579
|
+
if (sepIdx === -1) continue;
|
|
580
|
+
const pid = parseInt(line.substring(0, sepIdx).trim());
|
|
581
|
+
const memory = parseInt(line.substring(sepIdx + 1).trim()) || 0;
|
|
582
|
+
if (!isNaN(pid) && pidSet.has(pid)) {
|
|
583
|
+
resourceMap.set(pid, { memory, cpu: 0 });
|
|
537
584
|
}
|
|
538
585
|
}
|
|
539
586
|
} else {
|
|
@@ -595,14 +642,12 @@ export async function getProcessPorts(pid: number): Promise<number[]> {
|
|
|
595
642
|
if (ports.size > 0) return Array.from(ports);
|
|
596
643
|
} catch { /* ss not available, try lsof */ }
|
|
597
644
|
|
|
598
|
-
const result = await $`lsof -
|
|
645
|
+
const result = await $`lsof -Pan -p ${pid} -iTCP -sTCP:LISTEN`.nothrow().quiet().text();
|
|
599
646
|
const ports = new Set<number>();
|
|
600
647
|
for (const line of result.split('\n')) {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
ports.add(parseInt(portMatch[1]));
|
|
605
|
-
}
|
|
648
|
+
const portMatch = line.match(/:(\d+)\s+\(LISTEN\)/);
|
|
649
|
+
if (portMatch) {
|
|
650
|
+
ports.add(parseInt(portMatch[1]));
|
|
606
651
|
}
|
|
607
652
|
}
|
|
608
653
|
return Array.from(ports);
|
package/src/server.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* - Otherwise → defaults to 3000, falls back to next available if busy
|
|
15
15
|
*/
|
|
16
16
|
import path from 'path';
|
|
17
|
-
import { getAllProcesses, getProcess } from './db';
|
|
17
|
+
import { getAllProcesses, getProcess, addHistoryEntry } from './db';
|
|
18
18
|
import { isProcessRunning } from './platform';
|
|
19
19
|
import { handleRun } from './commands/run';
|
|
20
20
|
import { parseEnvString } from './utils';
|
|
@@ -26,8 +26,44 @@ const GUARD_SKIP_NAMES = new Set(['bgr-dashboard', 'bgr-guard']); // Don't try t
|
|
|
26
26
|
const _g = globalThis as any;
|
|
27
27
|
if (!_g.__bgrGuardRestartCounts) _g.__bgrGuardRestartCounts = new Map<string, number>();
|
|
28
28
|
if (!_g.__bgrGuardNextRestartTime) _g.__bgrGuardNextRestartTime = new Map<string, number>();
|
|
29
|
+
if (!_g.__bgrGuardEvents) _g.__bgrGuardEvents = [] as { time: number; name: string; action: string; success: boolean }[];
|
|
29
30
|
export const guardRestartCounts: Map<string, number> = _g.__bgrGuardRestartCounts;
|
|
30
31
|
const guardNextRestartTime: Map<string, number> = _g.__bgrGuardNextRestartTime;
|
|
32
|
+
export const guardEvents: { time: number; name: string; action: string; success: boolean }[] = _g.__bgrGuardEvents;
|
|
33
|
+
|
|
34
|
+
/** Try to free a port from zombie processes (dead PIDs holding sockets) */
|
|
35
|
+
async function cleanupPort(port: number): Promise<number> {
|
|
36
|
+
if (process.platform !== 'win32') return port;
|
|
37
|
+
try {
|
|
38
|
+
const proc = Bun.spawn(['powershell', '-NoProfile', '-Command',
|
|
39
|
+
`Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
|
|
40
|
+
], { stdout: 'pipe', stderr: 'pipe' });
|
|
41
|
+
const text = await new Response(proc.stdout).text();
|
|
42
|
+
const pid = parseInt(text.trim(), 10);
|
|
43
|
+
if (!pid || pid === process.pid) return port;
|
|
44
|
+
|
|
45
|
+
// Check if the owning process is actually dead
|
|
46
|
+
const checkProc = Bun.spawn(['powershell', '-NoProfile', '-Command',
|
|
47
|
+
`Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`
|
|
48
|
+
], { stdout: 'pipe', stderr: 'pipe' });
|
|
49
|
+
const checkText = await new Response(checkProc.stdout).text();
|
|
50
|
+
if (checkText.trim()) {
|
|
51
|
+
// Process is alive — kill it to reclaim the port
|
|
52
|
+
console.log(`[server] Killing PID ${pid} holding port ${port}`);
|
|
53
|
+
Bun.spawn(['taskkill', '/F', '/PID', String(pid)], { stdout: 'pipe', stderr: 'pipe' });
|
|
54
|
+
await Bun.sleep(1000);
|
|
55
|
+
return port;
|
|
56
|
+
} else {
|
|
57
|
+
// Process is dead but socket is zombie — use fallback port
|
|
58
|
+
const fallback = port + 1;
|
|
59
|
+
console.log(`[server] ⚠ Port ${port} held by zombie PID ${pid} — falling back to port ${fallback}`);
|
|
60
|
+
return fallback;
|
|
61
|
+
}
|
|
62
|
+
} catch { return port; /* best-effort cleanup */ }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let _originalPort = 3000;
|
|
66
|
+
let _currentPort = 3000;
|
|
31
67
|
|
|
32
68
|
export async function startServer() {
|
|
33
69
|
// Dynamic import to avoid melina's side-effect console.log at bundle load time
|
|
@@ -36,12 +72,20 @@ export async function startServer() {
|
|
|
36
72
|
|
|
37
73
|
// Only pass port when BUN_PORT is explicitly set.
|
|
38
74
|
// When omitted, Melina defaults to 3000 with auto-fallback to next available port.
|
|
39
|
-
const
|
|
75
|
+
const requestedPort = process.env.BUN_PORT ? parseInt(process.env.BUN_PORT, 10) : 3000;
|
|
76
|
+
_originalPort = requestedPort;
|
|
77
|
+
|
|
78
|
+
// Clean up zombie port bindings before starting — may return a different fallback port
|
|
79
|
+
const resolvedPort = await cleanupPort(requestedPort);
|
|
80
|
+
_currentPort = resolvedPort;
|
|
81
|
+
|
|
82
|
+
// Pass port explicitly if user requested one OR if we had to fallback
|
|
83
|
+
const needsExplicitPort = process.env.BUN_PORT || resolvedPort !== requestedPort;
|
|
40
84
|
await start({
|
|
41
85
|
appDir,
|
|
42
86
|
defaultTitle: 'bgrun Dashboard - Process Manager',
|
|
43
87
|
globalCss: path.join(appDir, 'globals.css'),
|
|
44
|
-
...(
|
|
88
|
+
...(needsExplicitPort && { port: resolvedPort }),
|
|
45
89
|
});
|
|
46
90
|
|
|
47
91
|
// Start the built-in process guard
|
|
@@ -50,6 +94,33 @@ export async function startServer() {
|
|
|
50
94
|
// Start log rotation (prevents unbounded log file growth)
|
|
51
95
|
const { startLogRotation } = await import('./log-rotation');
|
|
52
96
|
startLogRotation(() => getAllProcesses());
|
|
97
|
+
|
|
98
|
+
// Start sticky port checker - periodically try original port if we're on a fallback
|
|
99
|
+
if (resolvedPort !== requestedPort) {
|
|
100
|
+
startStickyPortChecker();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function startStickyPortChecker() {
|
|
105
|
+
const CHECK_INTERVAL_MS = 60_000; // Check every 60 seconds
|
|
106
|
+
console.log(`[server] Starting sticky port checker (original: ${_originalPort}, current: ${_currentPort})`);
|
|
107
|
+
|
|
108
|
+
setInterval(async () => {
|
|
109
|
+
if (_currentPort === _originalPort) return; // Already on original port
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const proc = Bun.spawn(['powershell', '-NoProfile', '-Command',
|
|
113
|
+
`Get-NetTCPConnection -LocalPort ${_originalPort} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
|
|
114
|
+
], { stdout: 'pipe', stderr: 'pipe' });
|
|
115
|
+
const text = await new Response(proc.stdout).text();
|
|
116
|
+
const pid = parseInt(text.trim(), 10);
|
|
117
|
+
|
|
118
|
+
if (!pid) {
|
|
119
|
+
console.log(`[server] ✓ Original port ${_originalPort} is now available! Consider restarting to reclaim it.`);
|
|
120
|
+
_currentPort = _originalPort;
|
|
121
|
+
}
|
|
122
|
+
} catch { /* best-effort */ }
|
|
123
|
+
}, CHECK_INTERVAL_MS);
|
|
53
124
|
}
|
|
54
125
|
|
|
55
126
|
/**
|
|
@@ -89,6 +160,7 @@ function startGuard() {
|
|
|
89
160
|
if (now < nextRestart) continue; // Still in backoff period
|
|
90
161
|
|
|
91
162
|
console.log(`[guard] ⚠ Guarded process "${proc.name}" (PID ${proc.pid}) is dead, restarting...`);
|
|
163
|
+
let success = false;
|
|
92
164
|
try {
|
|
93
165
|
await handleRun({
|
|
94
166
|
action: 'run',
|
|
@@ -96,12 +168,22 @@ function startGuard() {
|
|
|
96
168
|
force: true,
|
|
97
169
|
remoteName: '',
|
|
98
170
|
});
|
|
171
|
+
success = true;
|
|
99
172
|
|
|
100
173
|
// Track restart count
|
|
101
174
|
const prevCount = guardRestartCounts.get(proc.name) || 0;
|
|
102
175
|
const newCount = prevCount + 1;
|
|
103
176
|
guardRestartCounts.set(proc.name, newCount);
|
|
104
177
|
|
|
178
|
+
// Record in history database
|
|
179
|
+
try {
|
|
180
|
+
addHistoryEntry(proc.name, 'restart', undefined, { by: 'guard', count: newCount });
|
|
181
|
+
} catch { /* ignore history errors */ }
|
|
182
|
+
|
|
183
|
+
// Record event for dashboard
|
|
184
|
+
guardEvents.unshift({ time: now, name: proc.name, action: 'restart', success: true });
|
|
185
|
+
if (guardEvents.length > 100) guardEvents.pop();
|
|
186
|
+
|
|
105
187
|
// Exponential backoff if it crashes repeatedly (more than 5 times)
|
|
106
188
|
if (newCount > 5) {
|
|
107
189
|
const backoffSeconds = Math.min(30 * Math.pow(2, newCount - 6), 300); // 30s, 60s, 120s, up to 5 mins
|
|
@@ -112,6 +194,8 @@ function startGuard() {
|
|
|
112
194
|
}
|
|
113
195
|
} catch (err: any) {
|
|
114
196
|
console.error(`[guard] ✗ Failed to restart "${proc.name}": ${err.message}`);
|
|
197
|
+
guardEvents.unshift({ time: now, name: proc.name, action: 'restart', success: false });
|
|
198
|
+
if (guardEvents.length > 100) guardEvents.pop();
|
|
115
199
|
}
|
|
116
200
|
} else {
|
|
117
201
|
// Reset counter if process has been stable (alive at least once during check)
|
package/src/utils.ts
CHANGED
|
@@ -37,8 +37,8 @@ export async function getVersion(): Promise<string> {
|
|
|
37
37
|
|
|
38
38
|
export function validateDirectory(directory: string) {
|
|
39
39
|
if (!directory || !fs.existsSync(directory)) {
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
// Throw instead of process.exit() — lets dashboard API handlers catch gracefully
|
|
41
|
+
throw new Error(`Directory not found or invalid: '${directory}'`);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|