bgrun 3.3.3 → 3.7.0

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.
@@ -0,0 +1,55 @@
1
+ /**
2
+ * GET /api/config/:name — Read .config.toml content
3
+ * PUT /api/config/:name — Write .config.toml content
4
+ */
5
+ import { getProcess } from '../../../../../src/db';
6
+ import { join } from 'path';
7
+ import { readFile, writeFile } from 'fs/promises';
8
+
9
+ function resolveConfigPath(proc: any): string | null {
10
+ if (!proc.configPath) return null;
11
+ return join(proc.workdir, proc.configPath);
12
+ }
13
+
14
+ export async function GET(req: Request, { params }: { params: { name: string } }) {
15
+ const name = decodeURIComponent(params.name);
16
+ const proc = getProcess(name);
17
+
18
+ if (!proc) {
19
+ return Response.json({ error: 'Process not found' }, { status: 404 });
20
+ }
21
+
22
+ const configPath = resolveConfigPath(proc);
23
+ if (!configPath) {
24
+ return Response.json({ content: '', path: null, exists: false });
25
+ }
26
+
27
+ try {
28
+ const content = await readFile(configPath, 'utf-8');
29
+ return Response.json({ content, path: configPath, exists: true });
30
+ } catch {
31
+ return Response.json({ content: '', path: configPath, exists: false });
32
+ }
33
+ }
34
+
35
+ export async function PUT(req: Request, { params }: { params: { name: string } }) {
36
+ const name = decodeURIComponent(params.name);
37
+ const proc = getProcess(name);
38
+
39
+ if (!proc) {
40
+ return Response.json({ error: 'Process not found' }, { status: 404 });
41
+ }
42
+
43
+ const configPath = resolveConfigPath(proc);
44
+ if (!configPath) {
45
+ return Response.json({ error: 'No config path configured' }, { status: 400 });
46
+ }
47
+
48
+ try {
49
+ const body = await req.json();
50
+ await writeFile(configPath, body.content, 'utf-8');
51
+ return Response.json({ success: true, path: configPath });
52
+ } catch (e: any) {
53
+ return Response.json({ error: e.message }, { status: 500 });
54
+ }
55
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * GET /api/debug — Debug info about BGR internals
3
+ *
4
+ * Returns DB path, BGR home dir, platform info for diagnostics.
5
+ */
6
+ import { getDbInfo } from '../../../../src/db';
7
+ import { measureSync } from 'measure-fn';
8
+
9
+ export async function GET() {
10
+ const info = measureSync('DB info', () => getDbInfo());
11
+ return Response.json({
12
+ ...info,
13
+ platform: process.platform,
14
+ bun: Bun.version,
15
+ pid: process.pid,
16
+ cwd: process.cwd(),
17
+ });
18
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * GET /api/events — Server-Sent Events endpoint
3
+ *
4
+ * Streams process data every 3 seconds. Replaces client-side polling.
5
+ * The client connects once via EventSource and receives updates automatically.
6
+ *
7
+ * Delegates to /api/processes for data enrichment (including PID reconciliation).
8
+ */
9
+
10
+ const INTERVAL_MS = 3_000;
11
+
12
+ export async function GET(req: Request) {
13
+ const url = new URL(req.url);
14
+ const origin = url.origin; // e.g. http://localhost:3001
15
+
16
+ const encoder = new TextEncoder();
17
+
18
+ const stream = new ReadableStream({
19
+ async start(controller) {
20
+ // Send initial data immediately
21
+ try {
22
+ const res = await fetch(`${origin}/api/processes?t=${Date.now()}`);
23
+ const data = await res.json();
24
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
25
+ } catch {
26
+ controller.enqueue(encoder.encode(`data: []\n\n`));
27
+ }
28
+
29
+ controller.enqueue(encoder.encode(`: keepalive\n\n`));
30
+
31
+ // Then send updates every INTERVAL_MS
32
+ const interval = setInterval(async () => {
33
+ try {
34
+ const res = await fetch(`${origin}/api/processes?t=${Date.now()}`);
35
+ const data = await res.json();
36
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
37
+ } catch {
38
+ // Skip this tick, send on next
39
+ }
40
+ }, INTERVAL_MS);
41
+
42
+ // Store cleanup for when the stream is cancelled
43
+ (stream as any).__cleanup = () => clearInterval(interval);
44
+ },
45
+ cancel() {
46
+ if ((stream as any).__cleanup) {
47
+ (stream as any).__cleanup();
48
+ }
49
+ }
50
+ });
51
+
52
+ return new Response(stream, {
53
+ headers: {
54
+ 'Content-Type': 'text/event-stream',
55
+ 'Cache-Control': 'no-cache',
56
+ 'Connection': 'keep-alive',
57
+ 'X-Accel-Buffering': 'no',
58
+ },
59
+ });
60
+ }
@@ -1,7 +1,52 @@
1
1
  /**
2
- * GET /api/logs/:name — Read last 100 lines of process stdout/stderr
2
+ * GET /api/logs/:name — Read process stdout/stderr logs
3
+ *
4
+ * Supports incremental loading via query params:
5
+ * ?tab=stdout|stderr — which log to read (default: stdout)
6
+ * ?offset=N — byte offset to start reading from (default: 0 = full file)
7
+ *
8
+ * Returns:
9
+ * { text, size, mtime, filePath }
10
+ *
11
+ * On first call (offset=0), returns full file content.
12
+ * On subsequent calls (offset=previousSize), returns only new bytes.
13
+ * Client uses `size` as the offset for the next request.
3
14
  */
4
- import { getProcess, readFileTail } from 'bgrun';
15
+ import { getProcess } from '../../../../../src/db';
16
+ import { stat, open } from 'fs/promises';
17
+
18
+ interface FileInfo {
19
+ text: string;
20
+ size: number;
21
+ mtime: string | null;
22
+ filePath: string;
23
+ }
24
+
25
+ async function readLogFile(path: string, offset: number): Promise<FileInfo> {
26
+ try {
27
+ const s = await stat(path);
28
+ const size = s.size;
29
+ const mtime = s.mtime.toISOString();
30
+
31
+ // If offset >= current size, no new data
32
+ if (offset >= size) {
33
+ return { text: '', size, mtime, filePath: path };
34
+ }
35
+
36
+ // Read from offset to end
37
+ const handle = await open(path, 'r');
38
+ try {
39
+ const bytesToRead = size - offset;
40
+ const buffer = Buffer.alloc(bytesToRead);
41
+ await handle.read(buffer, 0, bytesToRead, offset);
42
+ return { text: buffer.toString('utf-8'), size, mtime, filePath: path };
43
+ } finally {
44
+ await handle.close();
45
+ }
46
+ } catch {
47
+ return { text: '', size: 0, mtime: null, filePath: path };
48
+ }
49
+ }
5
50
 
6
51
  export async function GET(req: Request, { params }: { params: { name: string } }) {
7
52
  const name = decodeURIComponent(params.name);
@@ -11,7 +56,12 @@ export async function GET(req: Request, { params }: { params: { name: string } }
11
56
  return Response.json({ error: 'Process not found' }, { status: 404 });
12
57
  }
13
58
 
14
- const stdout = await readFileTail(proc.stdout_path, 100);
15
- const stderr = await readFileTail(proc.stderr_path, 100);
16
- return Response.json({ stdout, stderr });
59
+ const url = new URL(req.url);
60
+ const tab = url.searchParams.get('tab') || 'stdout';
61
+ const offset = parseInt(url.searchParams.get('offset') || '0', 10) || 0;
62
+
63
+ const path = tab === 'stderr' ? proc.stderr_path : proc.stdout_path;
64
+ const info = await readLogFile(path, offset);
65
+
66
+ return Response.json(info);
17
67
  }
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * DELETE /api/processes/:name — Stop and remove a process
3
3
  */
4
- import { getProcess, removeProcessByName, isProcessRunning, terminateProcess } from 'bgrun';
4
+ import { getProcess, removeProcessByName } from '../../../../../src/db';
5
+ import { isProcessRunning, terminateProcess } from '../../../../../src/platform';
6
+ import { measure } from 'measure-fn';
5
7
 
6
8
  export async function DELETE(req: Request, { params }: { params: { name: string } }) {
7
9
  const name = decodeURIComponent(params.name);
@@ -12,8 +14,9 @@ export async function DELETE(req: Request, { params }: { params: { name: string
12
14
  }
13
15
 
14
16
  if (await isProcessRunning(proc.pid)) {
15
- await terminateProcess(proc.pid);
17
+ await measure(`Terminate "${name}" before delete`, () => terminateProcess(proc.pid));
16
18
  }
19
+
17
20
  removeProcessByName(name);
18
21
  return Response.json({ success: true });
19
22
  }
@@ -1,13 +1,11 @@
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';
1
+ import { getAllProcesses, updateProcessPid } from '../../../../src/db';
2
+ import { calculateRuntime } from '../../../../src/utils';
3
+ import { getProcessBatchMemory, reconcileProcessPids } from '../../../../src/platform';
4
+ import { measure, createMeasure } from 'measure-fn';
9
5
  import { $ } from 'bun';
10
6
 
7
+ const api = createMeasure('api');
8
+
11
9
  const CACHE_TTL_MS = 5_000;
12
10
  const SUBPROCESS_TIMEOUT_MS = 4_000;
13
11
 
@@ -97,38 +95,95 @@ async function getPortsByPid(pids: number[]): Promise<Map<number, number[]>> {
97
95
  return portMap;
98
96
  }
99
97
 
98
+ // Parse environment string to find BGR_GROUP
99
+ function getProcessGroup(envStr: string): string | null {
100
+ if (!envStr) return null;
101
+ // Env is usually "KEY=VAL,KEY2=VAL2"
102
+ const match = envStr.match(/(?:^|,)BGR_GROUP=([^,]+)/);
103
+ return match ? match[1] : null;
104
+ }
105
+
100
106
  async function fetchProcesses(): Promise<any[]> {
101
- const procs = getAllProcesses();
102
- const pids = procs.map((p: any) => p.pid);
107
+ return await api.measure('Fetch processes', async (m) => {
108
+ const procs = getAllProcesses();
109
+ const pids = procs.map((p: any) => p.pid);
103
110
 
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
- ]);
111
+ // Three subprocess calls total (not 3×N)
112
+ let [runningPids, portMap, memoryMap] = await Promise.all([
113
+ m('Running PIDs', () => withTimeout(getRunningPids(pids), new Set<number>())),
114
+ m('Port map', () => withTimeout(getPortsByPid(pids), new Map<number, number[]>())),
115
+ m('Memory map', () => withTimeout(getProcessBatchMemory(pids), new Map<number, number>())),
116
+ ]);
117
+
118
+ // PID reconciliation: if stored PIDs are dead, try to find the real process
119
+ const allPids = new Set(pids);
120
+ const deadPids = new Set(pids.filter((pid: number) => !runningPids?.has(pid)));
121
+
122
+ if (deadPids.size > 0) {
123
+ const reconciled = await m('Reconcile dead PIDs', () =>
124
+ withTimeout(reconcileProcessPids(procs, deadPids), new Map<string, number>())
125
+ );
126
+
127
+ if (reconciled && reconciled.size > 0) {
128
+ // Update stored PIDs in DB and refresh running status
129
+ const newPids: number[] = [];
130
+ for (const [name, newPid] of reconciled) {
131
+ updateProcessPid(name, newPid);
132
+ newPids.push(newPid);
133
+ // Update the proc object in-place so the response uses the new PID
134
+ const proc = procs.find((p: any) => p.name === name);
135
+ if (proc) proc.pid = newPid;
136
+ }
109
137
 
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
- });
138
+ // Mark reconciled PIDs as running
139
+ if (!runningPids) runningPids = new Set();
140
+ for (const pid of newPids) runningPids.add(pid);
141
+
142
+ // Re-fetch ports and memory for the new PIDs
143
+ const [newPorts, newMem] = await Promise.all([
144
+ withTimeout(getPortsByPid(newPids), new Map<number, number[]>()),
145
+ withTimeout(getProcessBatchMemory(newPids), new Map<number, number>()),
146
+ ]);
147
+ if (!portMap) portMap = new Map();
148
+ if (!memoryMap) memoryMap = new Map();
149
+ for (const [pid, ports] of newPorts) portMap.set(pid, ports);
150
+ for (const [pid, mem] of newMem) memoryMap.set(pid, mem);
151
+ }
152
+ }
153
+
154
+ return procs.map((p: any) => {
155
+ const running = runningPids?.has(p.pid) ?? false;
156
+ const ports = running ? (portMap?.get(p.pid) || []) : [];
157
+ const memory = running ? (memoryMap?.get(p.pid) || 0) : 0;
158
+
159
+ return {
160
+ name: p.name,
161
+ command: p.command,
162
+ directory: p.workdir,
163
+ pid: p.pid,
164
+ running,
165
+ port: ports.length > 0 ? ports[0] : null,
166
+ ports,
167
+ memory, // Bytes
168
+ group: getProcessGroup(p.env),
169
+ runtime: calculateRuntime(p.timestamp),
170
+ timestamp: p.timestamp,
171
+ env: p.env || '',
172
+ configPath: p.configPath || '',
173
+ stdoutPath: p.stdout_path || '',
174
+ stderrPath: p.stderr_path || '',
175
+ };
176
+ });
177
+ }) ?? [];
125
178
  }
126
179
 
127
- export async function GET() {
180
+ export async function GET(req: Request) {
181
+ const url = new URL(req.url);
182
+ const bustCache = url.searchParams.has('t');
128
183
  const now = Date.now();
129
184
 
130
- // Return cached data if still fresh
131
- if (cache.data && (now - cache.timestamp) < CACHE_TTL_MS) {
185
+ // Return cached data if still fresh and no bust param
186
+ if (!bustCache && cache.data && (now - cache.timestamp) < CACHE_TTL_MS) {
132
187
  return Response.json(cache.data);
133
188
  }
134
189
 
@@ -145,6 +200,11 @@ export async function GET() {
145
200
  });
146
201
  }
147
202
 
148
- const result = await cache.inflight;
149
- return Response.json(result);
203
+ try {
204
+ const result = await cache.inflight;
205
+ return Response.json(result);
206
+ } catch (err) {
207
+ console.error('[api/processes] Error fetching processes:', err);
208
+ return Response.json(cache.data ?? []);
209
+ }
150
210
  }
@@ -1,18 +1,19 @@
1
1
  /**
2
2
  * POST /api/restart/:name — Force-restart a process
3
3
  */
4
- import { handleRun } from 'bgrun';
4
+ import { handleRun } from '../../../../../src/commands/run';
5
+ import { measure } from 'measure-fn';
5
6
 
6
7
  export async function POST(req: Request, { params }: { params: { name: string } }) {
7
8
  const name = decodeURIComponent(params.name);
8
9
 
9
10
  try {
10
- await handleRun({
11
+ await measure(`Restart "${name}"`, () => handleRun({
11
12
  action: 'run',
12
13
  name,
13
14
  force: true,
14
15
  remoteName: '',
15
- });
16
+ }));
16
17
  return Response.json({ success: true });
17
18
  } catch (e: any) {
18
19
  return Response.json({ error: e.message }, { status: 500 });
@@ -1,20 +1,21 @@
1
1
  /**
2
2
  * POST /api/start — Create or start a process
3
3
  */
4
- import { handleRun } from 'bgrun';
4
+ import { handleRun } from '../../../../src/commands/run';
5
+ import { measure } from 'measure-fn';
5
6
 
6
7
  export async function POST(req: Request) {
7
8
  const body = await req.json();
8
9
 
9
10
  try {
10
- await handleRun({
11
+ await measure(`Start process "${body.name}"`, () => handleRun({
11
12
  action: 'run',
12
13
  name: body.name,
13
14
  command: body.command,
14
15
  directory: body.directory,
15
16
  force: body.force || false,
16
17
  remoteName: '',
17
- });
18
+ }));
18
19
  return Response.json({ success: true });
19
20
  } catch (e: any) {
20
21
  return Response.json({ error: e.message }, { status: 500 });
@@ -1,16 +1,23 @@
1
1
  /**
2
2
  * POST /api/stop/:name — Stop a running process
3
3
  */
4
- import { getProcess, isProcessRunning, terminateProcess } from 'bgrun';
4
+ import { getProcess } from '../../../../../src/db';
5
+ import { isProcessRunning, terminateProcess } from '../../../../../src/platform';
6
+ import { measure } from 'measure-fn';
5
7
 
6
8
  export async function POST(req: Request, { params }: { params: { name: string } }) {
7
9
  const name = decodeURIComponent(params.name);
8
10
  const proc = getProcess(name);
9
11
 
10
- if (!proc || !(await isProcessRunning(proc.pid))) {
11
- return Response.json({ error: 'Process not found or not running' }, { status: 404 });
12
+ if (!proc) {
13
+ return Response.json({ error: 'Process not found' }, { status: 404 });
12
14
  }
13
15
 
14
- await terminateProcess(proc.pid);
16
+ const running = await isProcessRunning(proc.pid);
17
+ if (!running) {
18
+ return Response.json({ error: 'Process not running' }, { status: 404 });
19
+ }
20
+
21
+ await measure(`Stop "${name}" (PID ${proc.pid})`, () => terminateProcess(proc.pid));
15
22
  return Response.json({ success: true });
16
23
  }
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * GET /api/version — Return BGR version
3
3
  */
4
- import { getVersion } from 'bgrun';
4
+ import { getVersion } from '../../../../src/utils';
5
+ import { measure } from 'measure-fn';
5
6
 
6
7
  export async function GET() {
7
- return Response.json({ version: await getVersion() });
8
+ const version = await measure('Get version', () => getVersion());
9
+ return Response.json({ version });
8
10
  }