bgrun 3.3.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 +720 -0
- package/dashboard/app/api/logs/[name]/route.ts +17 -0
- package/dashboard/app/api/processes/[name]/route.ts +19 -0
- package/dashboard/app/api/processes/route.ts +150 -0
- package/dashboard/app/api/restart/[name]/route.ts +20 -0
- package/dashboard/app/api/start/route.ts +22 -0
- package/dashboard/app/api/stop/[name]/route.ts +16 -0
- package/dashboard/app/api/version/route.ts +8 -0
- package/dashboard/app/globals.css +1135 -0
- package/dashboard/app/layout.tsx +47 -0
- package/dashboard/app/page.client.tsx +554 -0
- package/dashboard/app/page.tsx +130 -0
- package/dist/index.js +1580 -0
- package/examples/bgr-startup.sh +40 -0
- package/package.json +60 -0
- package/src/api.ts +31 -0
- package/src/build.ts +26 -0
- package/src/commands/cleanup.ts +142 -0
- package/src/commands/details.ts +46 -0
- package/src/commands/list.ts +86 -0
- package/src/commands/logs.ts +49 -0
- package/src/commands/run.ts +151 -0
- package/src/commands/watch.ts +223 -0
- package/src/config.ts +37 -0
- package/src/db.ts +115 -0
- package/src/index.ts +349 -0
- package/src/logger.ts +29 -0
- package/src/platform.ts +440 -0
- package/src/schema.ts +2 -0
- package/src/server.ts +24 -0
- package/src/table.ts +230 -0
- package/src/types.ts +27 -0
- package/src/utils.ts +99 -0
- package/src/version.macro.ts +17 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/logs/:name — Read last 100 lines of process stdout/stderr
|
|
3
|
+
*/
|
|
4
|
+
import { getProcess, readFileTail } from 'bgrun';
|
|
5
|
+
|
|
6
|
+
export async function GET(req: Request, { params }: { params: { name: string } }) {
|
|
7
|
+
const name = decodeURIComponent(params.name);
|
|
8
|
+
const proc = getProcess(name);
|
|
9
|
+
|
|
10
|
+
if (!proc) {
|
|
11
|
+
return Response.json({ error: 'Process not found' }, { status: 404 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const stdout = await readFileTail(proc.stdout_path, 100);
|
|
15
|
+
const stderr = await readFileTail(proc.stderr_path, 100);
|
|
16
|
+
return Response.json({ stdout, stderr });
|
|
17
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DELETE /api/processes/:name — Stop and remove a process
|
|
3
|
+
*/
|
|
4
|
+
import { getProcess, removeProcessByName, isProcessRunning, terminateProcess } from 'bgrun';
|
|
5
|
+
|
|
6
|
+
export async function DELETE(req: Request, { params }: { params: { name: string } }) {
|
|
7
|
+
const name = decodeURIComponent(params.name);
|
|
8
|
+
const proc = getProcess(name);
|
|
9
|
+
|
|
10
|
+
if (!proc) {
|
|
11
|
+
return Response.json({ error: 'Not found' }, { status: 404 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (await isProcessRunning(proc.pid)) {
|
|
15
|
+
await terminateProcess(proc.pid);
|
|
16
|
+
}
|
|
17
|
+
removeProcessByName(name);
|
|
18
|
+
return Response.json({ success: true });
|
|
19
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/processes — Enriched process list
|
|
3
|
+
*
|
|
4
|
+
* Uses batch subprocess calls (single tasklist + single netstat)
|
|
5
|
+
* instead of per-process calls to avoid subprocess pile-up on Windows.
|
|
6
|
+
* Results are cached for 5 seconds via globalThis.
|
|
7
|
+
*/
|
|
8
|
+
import { getAllProcesses, calculateRuntime } from 'bgrun';
|
|
9
|
+
import { $ } from 'bun';
|
|
10
|
+
|
|
11
|
+
const CACHE_TTL_MS = 5_000;
|
|
12
|
+
const SUBPROCESS_TIMEOUT_MS = 4_000;
|
|
13
|
+
|
|
14
|
+
// Persistent cache across module re-evaluations
|
|
15
|
+
const g = globalThis as any;
|
|
16
|
+
if (!g.__bgrProcessCache) {
|
|
17
|
+
g.__bgrProcessCache = { data: null, timestamp: 0, inflight: null };
|
|
18
|
+
}
|
|
19
|
+
const cache = g.__bgrProcessCache;
|
|
20
|
+
|
|
21
|
+
function withTimeout<T>(promise: Promise<T>, fallback: T): Promise<T> {
|
|
22
|
+
return Promise.race([
|
|
23
|
+
promise,
|
|
24
|
+
new Promise<T>(resolve => setTimeout(() => resolve(fallback), SUBPROCESS_TIMEOUT_MS)),
|
|
25
|
+
]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Single tasklist call — returns set of running PIDs */
|
|
29
|
+
async function getRunningPids(pids: number[]): Promise<Set<number>> {
|
|
30
|
+
if (pids.length === 0) return new Set();
|
|
31
|
+
try {
|
|
32
|
+
const isWin = process.platform === 'win32';
|
|
33
|
+
if (isWin) {
|
|
34
|
+
const result = await $`tasklist /FO CSV /NH`.nothrow().quiet().text();
|
|
35
|
+
const runningPids = new Set<number>();
|
|
36
|
+
for (const line of result.split('\n')) {
|
|
37
|
+
const match = line.match(/"[^"]*","(\d+)"/);
|
|
38
|
+
if (match) {
|
|
39
|
+
const pid = parseInt(match[1]);
|
|
40
|
+
if (pids.includes(pid)) runningPids.add(pid);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return runningPids;
|
|
44
|
+
} else {
|
|
45
|
+
const result = await $`ps -p ${pids.join(',')} -o pid=`.nothrow().quiet().text();
|
|
46
|
+
const runningPids = new Set<number>();
|
|
47
|
+
for (const line of result.trim().split('\n')) {
|
|
48
|
+
const pid = parseInt(line.trim());
|
|
49
|
+
if (!isNaN(pid)) runningPids.add(pid);
|
|
50
|
+
}
|
|
51
|
+
return runningPids;
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
return new Set();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Single netstat call — returns map of PID → ports */
|
|
59
|
+
async function getPortsByPid(pids: number[]): Promise<Map<number, number[]>> {
|
|
60
|
+
const portMap = new Map<number, number[]>();
|
|
61
|
+
if (pids.length === 0) return portMap;
|
|
62
|
+
try {
|
|
63
|
+
const isWin = process.platform === 'win32';
|
|
64
|
+
if (isWin) {
|
|
65
|
+
const result = await $`netstat -ano`.nothrow().quiet().text();
|
|
66
|
+
const pidSet = new Set(pids);
|
|
67
|
+
for (const line of result.split('\n')) {
|
|
68
|
+
const match = line.match(/^\s*TCP\s+\S+:(\d+)\s+\S+\s+LISTENING\s+(\d+)/);
|
|
69
|
+
if (match) {
|
|
70
|
+
const port = parseInt(match[1]);
|
|
71
|
+
const pid = parseInt(match[2]);
|
|
72
|
+
if (pidSet.has(pid)) {
|
|
73
|
+
const existing = portMap.get(pid) || [];
|
|
74
|
+
if (!existing.includes(port)) existing.push(port);
|
|
75
|
+
portMap.set(pid, existing);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
const result = await $`ss -tlnp`.nothrow().quiet().text();
|
|
81
|
+
const pidSet = new Set(pids);
|
|
82
|
+
for (const line of result.split('\n')) {
|
|
83
|
+
for (const pid of pidSet) {
|
|
84
|
+
if (line.includes(`pid=${pid}`)) {
|
|
85
|
+
const portMatch = line.match(/:(\d+)\s/);
|
|
86
|
+
if (portMatch) {
|
|
87
|
+
const port = parseInt(portMatch[1]);
|
|
88
|
+
const existing = portMap.get(pid) || [];
|
|
89
|
+
if (!existing.includes(port)) existing.push(port);
|
|
90
|
+
portMap.set(pid, existing);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch { /* ignore */ }
|
|
97
|
+
return portMap;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function fetchProcesses(): Promise<any[]> {
|
|
101
|
+
const procs = getAllProcesses();
|
|
102
|
+
const pids = procs.map((p: any) => p.pid);
|
|
103
|
+
|
|
104
|
+
// Two subprocess calls total (not 2×N)
|
|
105
|
+
const [runningPids, portMap] = await Promise.all([
|
|
106
|
+
withTimeout(getRunningPids(pids), new Set<number>()),
|
|
107
|
+
withTimeout(getPortsByPid(pids), new Map<number, number[]>()),
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
return procs.map((p: any) => {
|
|
111
|
+
const running = runningPids.has(p.pid);
|
|
112
|
+
const ports = running ? (portMap.get(p.pid) || []) : [];
|
|
113
|
+
return {
|
|
114
|
+
name: p.name,
|
|
115
|
+
command: p.command,
|
|
116
|
+
directory: p.workdir,
|
|
117
|
+
pid: p.pid,
|
|
118
|
+
running,
|
|
119
|
+
port: ports.length > 0 ? ports[0] : null,
|
|
120
|
+
ports,
|
|
121
|
+
runtime: calculateRuntime(p.timestamp),
|
|
122
|
+
timestamp: p.timestamp,
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function GET() {
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
|
|
130
|
+
// Return cached data if still fresh
|
|
131
|
+
if (cache.data && (now - cache.timestamp) < CACHE_TTL_MS) {
|
|
132
|
+
return Response.json(cache.data);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Deduplicate concurrent requests
|
|
136
|
+
if (!cache.inflight) {
|
|
137
|
+
cache.inflight = fetchProcesses().then(result => {
|
|
138
|
+
cache.data = result;
|
|
139
|
+
cache.timestamp = Date.now();
|
|
140
|
+
cache.inflight = null;
|
|
141
|
+
return result;
|
|
142
|
+
}).catch(err => {
|
|
143
|
+
cache.inflight = null;
|
|
144
|
+
throw err;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const result = await cache.inflight;
|
|
149
|
+
return Response.json(result);
|
|
150
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/restart/:name — Force-restart a process
|
|
3
|
+
*/
|
|
4
|
+
import { handleRun } from 'bgrun';
|
|
5
|
+
|
|
6
|
+
export async function POST(req: Request, { params }: { params: { name: string } }) {
|
|
7
|
+
const name = decodeURIComponent(params.name);
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
await handleRun({
|
|
11
|
+
action: 'run',
|
|
12
|
+
name,
|
|
13
|
+
force: true,
|
|
14
|
+
remoteName: '',
|
|
15
|
+
});
|
|
16
|
+
return Response.json({ success: true });
|
|
17
|
+
} catch (e: any) {
|
|
18
|
+
return Response.json({ error: e.message }, { status: 500 });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/start — Create or start a process
|
|
3
|
+
*/
|
|
4
|
+
import { handleRun } from 'bgrun';
|
|
5
|
+
|
|
6
|
+
export async function POST(req: Request) {
|
|
7
|
+
const body = await req.json();
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
await handleRun({
|
|
11
|
+
action: 'run',
|
|
12
|
+
name: body.name,
|
|
13
|
+
command: body.command,
|
|
14
|
+
directory: body.directory,
|
|
15
|
+
force: body.force || false,
|
|
16
|
+
remoteName: '',
|
|
17
|
+
});
|
|
18
|
+
return Response.json({ success: true });
|
|
19
|
+
} catch (e: any) {
|
|
20
|
+
return Response.json({ error: e.message }, { status: 500 });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/stop/:name — Stop a running process
|
|
3
|
+
*/
|
|
4
|
+
import { getProcess, isProcessRunning, terminateProcess } from 'bgrun';
|
|
5
|
+
|
|
6
|
+
export async function POST(req: Request, { params }: { params: { name: string } }) {
|
|
7
|
+
const name = decodeURIComponent(params.name);
|
|
8
|
+
const proc = getProcess(name);
|
|
9
|
+
|
|
10
|
+
if (!proc || !(await isProcessRunning(proc.pid))) {
|
|
11
|
+
return Response.json({ error: 'Process not found or not running' }, { status: 404 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
await terminateProcess(proc.pid);
|
|
15
|
+
return Response.json({ success: true });
|
|
16
|
+
}
|