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/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
**Production-ready process manager with dashboard and programmatic API, designed for running your containers, services, and AI agents.**
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/bgrun)
|
|
8
|
+
[](https://github.com/Mements/bgr/actions/workflows/ci.yml)
|
|
8
9
|
[](https://bun.sh/)
|
|
9
10
|
[](./LICENSE)
|
|
10
11
|
|
|
@@ -74,11 +75,16 @@ bgrun.yourdomain.com {
|
|
|
74
75
|
Features:
|
|
75
76
|
- Real-time process status via SSE (no polling)
|
|
76
77
|
- Start, stop, restart, and delete processes from the UI
|
|
77
|
-
- Live stdout/stderr log viewer with search
|
|
78
|
+
- Live stdout/stderr log viewer with search and virtual scrolling
|
|
78
79
|
- Memory, PID, port, runtime, and guard restarts at a glance
|
|
79
80
|
- Guard toggle per-process (auto-restart on crash)
|
|
80
|
-
-
|
|
81
|
+
- Keyboard shortcuts — `↑/↓` or `j/k` navigate, `Enter` open, `R` restart, `S` stop, `G` guard, `D` delete, `N` new, `?` help
|
|
82
|
+
- Search with debounce, result count badge, and persistence across SSE updates
|
|
83
|
+
- Auto-calibrated virtual scroll for large log files (10K+ lines)
|
|
84
|
+
- Dark / light theme toggle
|
|
85
|
+
- Responsive mobile layout with cards view
|
|
81
86
|
- Collapsible directory groups
|
|
87
|
+
- Right-click context menu on process rows
|
|
82
88
|
|
|
83
89
|
---
|
|
84
90
|
|
|
@@ -26,7 +26,12 @@ export async function GET(req: Request) {
|
|
|
26
26
|
controller.enqueue(encoder.encode(`data: []\n\n`));
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
// Periodic keepalive to prevent proxy/browser timeouts
|
|
30
|
+
const keepaliveInterval = setInterval(() => {
|
|
31
|
+
try {
|
|
32
|
+
controller.enqueue(encoder.encode(`: keepalive\n\n`));
|
|
33
|
+
} catch { /* stream closed */ }
|
|
34
|
+
}, 15_000);
|
|
30
35
|
|
|
31
36
|
// Then send updates every INTERVAL_MS
|
|
32
37
|
const interval = setInterval(async () => {
|
|
@@ -40,7 +45,10 @@ export async function GET(req: Request) {
|
|
|
40
45
|
}, INTERVAL_MS);
|
|
41
46
|
|
|
42
47
|
// Store cleanup for when the stream is cancelled
|
|
43
|
-
(stream as any).__cleanup = () =>
|
|
48
|
+
(stream as any).__cleanup = () => {
|
|
49
|
+
clearInterval(interval);
|
|
50
|
+
clearInterval(keepaliveInterval);
|
|
51
|
+
};
|
|
44
52
|
},
|
|
45
53
|
cancel() {
|
|
46
54
|
if ((stream as any).__cleanup) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* When enabled=true, the built-in guard will auto-restart this process if it dies.
|
|
6
6
|
* When enabled=false, the process is left alone.
|
|
7
7
|
*/
|
|
8
|
-
import { getProcess, updateProcessEnv } from '../../../../src/db';
|
|
8
|
+
import { getProcess, updateProcessEnv, addHistoryEntry } from '../../../../src/db';
|
|
9
9
|
|
|
10
10
|
export async function POST(req: Request) {
|
|
11
11
|
try {
|
|
@@ -35,6 +35,9 @@ export async function POST(req: Request) {
|
|
|
35
35
|
// Save back
|
|
36
36
|
updateProcessEnv(body.name, JSON.stringify(env));
|
|
37
37
|
|
|
38
|
+
// Record history
|
|
39
|
+
addHistoryEntry(body.name, body.enabled ? 'guard_on' : 'guard_off');
|
|
40
|
+
|
|
38
41
|
return Response.json({
|
|
39
42
|
ok: true,
|
|
40
43
|
name: body.name,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getProcessHistory, getRecentHistory, addHistoryEntry } from '../../../../src/db';
|
|
2
|
+
|
|
3
|
+
export async function GET(req: Request) {
|
|
4
|
+
const url = new URL(req.url);
|
|
5
|
+
const name = url.searchParams.get('name');
|
|
6
|
+
const limit = parseInt(url.searchParams.get('limit') || '50');
|
|
7
|
+
|
|
8
|
+
let history;
|
|
9
|
+
if (name) {
|
|
10
|
+
history = getProcessHistory(name, limit);
|
|
11
|
+
} else {
|
|
12
|
+
history = getRecentHistory(limit);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return Response.json(history.map((h: any) => ({
|
|
16
|
+
process_name: h.process_name,
|
|
17
|
+
event: h.event,
|
|
18
|
+
pid: h.pid,
|
|
19
|
+
timestamp: h.timestamp,
|
|
20
|
+
metadata: h.metadata ? JSON.parse(h.metadata) : {},
|
|
21
|
+
})));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function POST(req: Request) {
|
|
25
|
+
try {
|
|
26
|
+
const body = await req.json();
|
|
27
|
+
const { process_name, event, pid, metadata } = body;
|
|
28
|
+
|
|
29
|
+
if (!process_name || !event) {
|
|
30
|
+
return Response.json({ error: 'process_name and event are required' }, { status: 400 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
addHistoryEntry(process_name, event, pid, metadata);
|
|
34
|
+
return Response.json({ success: true });
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error('[api/history] Error adding history:', err);
|
|
37
|
+
return Response.json({ error: 'Failed to add history' }, { status: 500 });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -237,10 +237,11 @@ async function fetchProcesses(): Promise<any[]> {
|
|
|
237
237
|
export async function GET(req: Request) {
|
|
238
238
|
const url = new URL(req.url);
|
|
239
239
|
const bustCache = url.searchParams.has('t');
|
|
240
|
+
const portFilter = url.searchParams.get('port');
|
|
240
241
|
const now = Date.now();
|
|
241
242
|
|
|
242
|
-
// Return cached data if still fresh and no bust param
|
|
243
|
-
if (!bustCache && cache.data && (now - cache.timestamp) < CACHE_TTL_MS) {
|
|
243
|
+
// Return cached data if still fresh and no bust param and no port filter
|
|
244
|
+
if (!bustCache && !portFilter && cache.data && (now - cache.timestamp) < CACHE_TTL_MS) {
|
|
244
245
|
return Response.json(cache.data);
|
|
245
246
|
}
|
|
246
247
|
|
|
@@ -258,7 +259,14 @@ export async function GET(req: Request) {
|
|
|
258
259
|
}
|
|
259
260
|
|
|
260
261
|
try {
|
|
261
|
-
|
|
262
|
+
let result = await cache.inflight;
|
|
263
|
+
// Filter by port if specified (also ensures fresh data by bypassing cache above)
|
|
264
|
+
if (portFilter) {
|
|
265
|
+
const portNum = parseInt(portFilter);
|
|
266
|
+
if (!isNaN(portNum)) {
|
|
267
|
+
result = result.filter((p: any) => p.ports?.includes(portNum));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
262
270
|
return Response.json(result);
|
|
263
271
|
} catch (err) {
|
|
264
272
|
console.error('[api/processes] Error fetching processes:', err);
|
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
* POST /api/restart/:name — Force-restart a process
|
|
3
3
|
*/
|
|
4
4
|
import { handleRun } from '../../../../../src/commands/run';
|
|
5
|
+
import { addHistoryEntry, getProcess } from '../../../../../src/db';
|
|
5
6
|
import { measure } from 'measure-fn';
|
|
6
7
|
|
|
7
8
|
export async function POST(req: Request, { params }: { params: { name: string } }) {
|
|
8
9
|
const name = decodeURIComponent(params.name);
|
|
10
|
+
const proc = getProcess(name);
|
|
11
|
+
const oldPid = proc?.pid;
|
|
9
12
|
|
|
10
13
|
try {
|
|
11
14
|
await measure(`Restart "${name}"`, () => handleRun({
|
|
@@ -14,6 +17,10 @@ export async function POST(req: Request, { params }: { params: { name: string }
|
|
|
14
17
|
force: true,
|
|
15
18
|
remoteName: '',
|
|
16
19
|
}));
|
|
20
|
+
|
|
21
|
+
// Record history
|
|
22
|
+
addHistoryEntry(name, 'restart', oldPid);
|
|
23
|
+
|
|
17
24
|
return Response.json({ success: true });
|
|
18
25
|
} catch (e: any) {
|
|
19
26
|
return Response.json({ error: e.message }, { status: 500 });
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* POST /api/start — Create or start a process
|
|
3
3
|
*/
|
|
4
4
|
import { handleRun } from '../../../../src/commands/run';
|
|
5
|
+
import { addHistoryEntry } from '../../../../src/db';
|
|
5
6
|
import { measure } from 'measure-fn';
|
|
6
7
|
|
|
7
8
|
export async function POST(req: Request) {
|
|
@@ -16,6 +17,10 @@ export async function POST(req: Request) {
|
|
|
16
17
|
force: body.force || false,
|
|
17
18
|
remoteName: '',
|
|
18
19
|
}));
|
|
20
|
+
|
|
21
|
+
// Record history
|
|
22
|
+
addHistoryEntry(body.name, 'start');
|
|
23
|
+
|
|
19
24
|
return Response.json({ success: true });
|
|
20
25
|
} catch (e: any) {
|
|
21
26
|
return Response.json({ error: e.message }, { status: 500 });
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Kills the registered PID, then kills anything remaining on the port.
|
|
5
5
|
* Sets PID to 0 to prevent reconciliation from hijacking unrelated processes.
|
|
6
6
|
*/
|
|
7
|
-
import { getProcess, updateProcessPid } from '../../../../../src/db';
|
|
7
|
+
import { getProcess, updateProcessPid, addHistoryEntry } from '../../../../../src/db';
|
|
8
8
|
import { isProcessRunning, terminateProcess, getProcessPorts, killProcessOnPort } from '../../../../../src/platform';
|
|
9
9
|
import { measure } from 'measure-fn';
|
|
10
10
|
|
|
@@ -37,5 +37,8 @@ export async function POST(req: Request, { params }: { params: { name: string }
|
|
|
37
37
|
// a random matching process as this one
|
|
38
38
|
updateProcessPid(name, 0);
|
|
39
39
|
|
|
40
|
+
// Record history
|
|
41
|
+
addHistoryEntry(name, 'stop', proc.pid);
|
|
42
|
+
|
|
40
43
|
return Response.json({ success: true });
|
|
41
44
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { getAllTemplates, saveTemplate, deleteTemplate } from '../../../../src/db';
|
|
2
|
+
|
|
3
|
+
export async function GET() {
|
|
4
|
+
const templates = getAllTemplates();
|
|
5
|
+
return Response.json(templates.map((t: any) => ({
|
|
6
|
+
name: t.name,
|
|
7
|
+
command: t.command,
|
|
8
|
+
workdir: t.workdir,
|
|
9
|
+
env: t.env,
|
|
10
|
+
group: t.group,
|
|
11
|
+
created_at: t.created_at,
|
|
12
|
+
})));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function POST(req: Request) {
|
|
16
|
+
try {
|
|
17
|
+
const body = await req.json();
|
|
18
|
+
const { name, command, workdir, env, group } = body;
|
|
19
|
+
|
|
20
|
+
if (!name || !command) {
|
|
21
|
+
return Response.json({ error: 'name and command are required' }, { status: 400 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
saveTemplate({ name, command, workdir, env, group });
|
|
25
|
+
return Response.json({ success: true, name });
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error('[api/templates] Error saving template:', err);
|
|
28
|
+
return Response.json({ error: 'Failed to save template' }, { status: 500 });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function DELETE(req: Request) {
|
|
33
|
+
try {
|
|
34
|
+
const url = new URL(req.url);
|
|
35
|
+
const name = url.searchParams.get('name');
|
|
36
|
+
|
|
37
|
+
if (!name) {
|
|
38
|
+
return Response.json({ error: 'name is required' }, { status: 400 });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
deleteTemplate(name);
|
|
42
|
+
return Response.json({ success: true });
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error('[api/templates] Error deleting template:', err);
|
|
45
|
+
return Response.json({ error: 'Failed to delete template' }, { status: 500 });
|
|
46
|
+
}
|
|
47
|
+
}
|