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.
@@ -0,0 +1,35 @@
1
+ /**
2
+ * GET /api/check-port?port=3001 — Check if a port is in use
3
+ *
4
+ * Attempts a TCP connect to localhost:port. Returns { inUse, port }.
5
+ */
6
+
7
+ export async function GET(req: Request) {
8
+ const url = new URL(req.url);
9
+ const port = parseInt(url.searchParams.get('port') || '0');
10
+
11
+ if (!port || port < 1 || port > 65535) {
12
+ return Response.json({ error: 'Invalid port' }, { status: 400 });
13
+ }
14
+
15
+ const inUse = await isPortInUse(port);
16
+ return Response.json({ port, inUse });
17
+ }
18
+
19
+ async function isPortInUse(port: number): Promise<boolean> {
20
+ return new Promise((resolve) => {
21
+ try {
22
+ const server = Bun.serve({
23
+ port,
24
+ hostname: '127.0.0.1',
25
+ fetch() { return new Response(''); },
26
+ });
27
+ // If we successfully bound, it's free
28
+ server.stop(true);
29
+ resolve(false);
30
+ } catch {
31
+ // Port is in use
32
+ resolve(true);
33
+ }
34
+ });
35
+ }
@@ -0,0 +1,40 @@
1
+ import { getDependencyGraph, addDependency, removeDependency, getStartOrder, getAllProcesses } from "../../../../src/db";
2
+
3
+ /** GET /api/dependencies — full dependency graph + start order */
4
+ export function GET() {
5
+ const graph = getDependencyGraph();
6
+ const startOrder = getStartOrder();
7
+ const processes = getAllProcesses().map(p => ({
8
+ name: p.name,
9
+ group: p.group || '',
10
+ pid: p.pid,
11
+ }));
12
+
13
+ return Response.json({ graph, startOrder, processes });
14
+ }
15
+
16
+ /** POST /api/dependencies — add a dependency */
17
+ export async function POST(req: Request) {
18
+ const body = await req.json() as { process: string; depends_on: string };
19
+ if (!body.process || !body.depends_on) {
20
+ return Response.json({ error: 'Missing process or depends_on' }, { status: 400 });
21
+ }
22
+
23
+ const ok = addDependency(body.process, body.depends_on);
24
+ if (!ok) {
25
+ return Response.json({ error: 'Invalid dependency (duplicate, self-reference, or would create cycle)' }, { status: 400 });
26
+ }
27
+
28
+ return Response.json({ ok: true });
29
+ }
30
+
31
+ /** DELETE /api/dependencies — remove a dependency */
32
+ export async function DELETE(req: Request) {
33
+ const body = await req.json() as { process: string; depends_on: string };
34
+ if (!body.process || !body.depends_on) {
35
+ return Response.json({ error: 'Missing process or depends_on' }, { status: 400 });
36
+ }
37
+
38
+ removeDependency(body.process, body.depends_on);
39
+ return Response.json({ ok: true });
40
+ }
@@ -4,54 +4,19 @@
4
4
  * Only works if the process directory is a git repository.
5
5
  * Steps: git pull → bun install → force restart
6
6
  */
7
- import { getProcess } from '../../../../../src/db';
8
- import { handleRun } from '../../../../../src/commands/run';
9
- import { measure } from 'measure-fn';
10
- import { $ } from 'bun';
7
+ import { deployProcess } from '../../../../../src/deploy';
11
8
 
12
9
  export async function POST(req: Request, { params }: { params: { name: string } }) {
13
10
  const name = decodeURIComponent(params.name);
14
11
 
15
12
  try {
16
- const proc = getProcess(name);
17
- if (!proc) {
18
- return Response.json({ error: `Process '${name}' not found` }, { status: 404 });
13
+ const result = await deployProcess(name);
14
+ if (result.ok) {
15
+ return Response.json({ success: true, ...result });
19
16
  }
20
17
 
21
- const dir = proc.workdir;
22
-
23
- // Check if it's a git repo
24
- const isGit = await Bun.file(`${dir}/.git/HEAD`).exists();
25
- if (!isGit) {
26
- return Response.json({ error: `'${dir}' is not a git repository` }, { status: 400 });
27
- }
28
-
29
- const result = await measure(`Deploy "${name}"`, async () => {
30
- // 1. Git pull
31
- $.cwd(dir);
32
- const pullOutput = await $`git pull`.text();
33
-
34
- // 2. Install dependencies (detect package manager)
35
- let installOutput = '';
36
- const hasBunLock = await Bun.file(`${dir}/bun.lock`).exists() || await Bun.file(`${dir}/bun.lockb`).exists();
37
- const hasPackageJson = await Bun.file(`${dir}/package.json`).exists();
38
-
39
- if (hasPackageJson) {
40
- installOutput = await $`bun install`.text();
41
- }
42
-
43
- // 3. Restart the process
44
- await handleRun({
45
- action: 'run',
46
- name,
47
- force: true,
48
- remoteName: '',
49
- });
50
-
51
- return { pullOutput: pullOutput.trim(), installOutput: installOutput.trim() };
52
- });
53
-
54
- return Response.json({ success: true, ...result });
18
+ const status = result.skipped ? 400 : 500;
19
+ return Response.json({ error: result.reason || `Failed to deploy '${name}'`, ...result }, { status });
55
20
  } catch (e: any) {
56
21
  return Response.json({ error: e.message }, { status: 500 });
57
22
  }
@@ -0,0 +1,25 @@
1
+ import { deployAllProcesses } from '../../../../src/deploy';
2
+
3
+ export async function POST(req: Request) {
4
+ try {
5
+ const body = await req.json().catch(() => ({}));
6
+ const group = body?.group ? String(body.group) : undefined;
7
+ const results = await deployAllProcesses(group);
8
+
9
+ const deployed = results.filter(r => r.ok);
10
+ const skipped = results.filter(r => r.skipped);
11
+ const failed = results.filter(r => !r.ok && !r.skipped);
12
+
13
+ return Response.json({
14
+ success: failed.length === 0,
15
+ group: group || null,
16
+ total: results.length,
17
+ deployed: deployed.length,
18
+ skipped: skipped.length,
19
+ failed: failed.length,
20
+ results,
21
+ }, { status: failed.length > 0 ? 207 : 200 });
22
+ } catch (e: any) {
23
+ return Response.json({ error: e?.message || String(e) }, { status: 500 });
24
+ }
25
+ }
@@ -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,5 @@
1
+ import { guardEvents } from '../../../../src/server';
2
+
3
+ export async function GET() {
4
+ return Response.json(guardEvents);
5
+ }
@@ -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
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * GET /api/next-port — Find the next available port
3
+ *
4
+ * Scans existing processes' env for PORT= values,
5
+ * then returns the next unused port starting from a base (default 3001).
6
+ */
7
+ import { getAllProcesses } from '../../../../src/db';
8
+
9
+ export async function GET(req: Request) {
10
+ const url = new URL(req.url);
11
+ const base = parseInt(url.searchParams.get('base') || '3001') || 3001;
12
+
13
+ const processes = getAllProcesses();
14
+ const usedPorts = new Set<number>();
15
+
16
+ for (const proc of processes) {
17
+ // Parse PORT from env string (comma-separated KEY=VAL)
18
+ const envStr = proc.env || '';
19
+ const portMatch = envStr.match(/(?:^|,)PORT=(\d+)/);
20
+ if (portMatch) {
21
+ usedPorts.add(parseInt(portMatch[1]));
22
+ }
23
+ }
24
+
25
+ // Find next available port
26
+ let nextPort = base;
27
+ while (usedPorts.has(nextPort)) {
28
+ nextPort++;
29
+ }
30
+
31
+ return Response.json({ port: nextPort, usedPorts: Array.from(usedPorts).sort((a, b) => a - b) });
32
+ }
@@ -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
- const result = await cache.inflight;
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,20 +2,31 @@
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) {
8
9
  const body = await req.json();
9
10
 
10
11
  try {
12
+ // Build env from body.env object if provided (e.g. { PORT: "3001" })
13
+ const env = body.env && typeof body.env === 'object'
14
+ ? body.env as Record<string, string>
15
+ : undefined;
16
+
11
17
  await measure(`Start process "${body.name}"`, () => handleRun({
12
18
  action: 'run',
13
19
  name: body.name,
14
20
  command: body.command,
15
21
  directory: body.directory,
16
22
  force: body.force || false,
23
+ env,
17
24
  remoteName: '',
18
25
  }));
26
+
27
+ // Record history
28
+ addHistoryEntry(body.name, 'start');
29
+
19
30
  return Response.json({ success: true });
20
31
  } catch (e: any) {
21
32
  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
+ }