bgrun 3.10.2 → 3.11.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.
package/package.json CHANGED
@@ -1,60 +1,61 @@
1
- {
2
- "name": "bgrun",
3
- "version": "3.10.2",
4
- "description": "bgrun — A lightweight process manager for Bun",
5
- "type": "module",
6
- "main": "./src/api.ts",
7
- "exports": {
8
- ".": "./src/api.ts"
9
- },
10
- "bin": {
11
- "bgrun": "./dist/index.js"
12
- },
13
- "scripts": {
14
- "build": "bun run ./src/build.ts",
15
- "test": "bun test",
16
- "prepublishOnly": "bun run build"
17
- },
18
- "files": [
19
- "dist",
20
- "src",
21
- "dashboard/app",
22
- "README.md",
23
- "image.png",
24
- "examples/bgr-startup.sh"
25
- ],
26
- "keywords": [
27
- "process-manager",
28
- "bun",
29
- "monitoring",
30
- "devops",
31
- "deployment",
32
- "background",
33
- "daemon"
34
- ],
35
- "author": "7flash",
36
- "license": "MIT",
37
- "repository": {
38
- "type": "git",
39
- "url": "https://github.com/7flash/bgrun.git"
40
- },
41
- "devDependencies": {
42
- "bun-types": "latest"
43
- },
44
- "peerDependencies": {
45
- "typescript": "^5.0.0"
46
- },
47
- "dependencies": {
48
- "boxen": "^8.0.1",
49
- "chalk": "^5.4.1",
50
- "dedent": "^1.5.3",
51
- "measure-fn": "^3.2.1",
52
- "melina": "^2.2.1",
53
- "react": "^19.2.4",
54
- "react-dom": "^19.2.4",
55
- "sqlite-zod-orm": "^3.8.0"
56
- },
57
- "engines": {
58
- "bun": ">=1.0.0"
59
- }
60
- }
1
+ {
2
+ "name": "bgrun",
3
+ "version": "3.11.0",
4
+ "description": "bgrun — A lightweight process manager for Bun",
5
+ "type": "module",
6
+ "main": "./src/api.ts",
7
+ "exports": {
8
+ ".": "./src/api.ts"
9
+ },
10
+ "bin": {
11
+ "bgrun": "./dist/index.js"
12
+ },
13
+ "scripts": {
14
+ "build": "bun run ./src/build.ts",
15
+ "test": "bun test",
16
+ "prepublishOnly": "bun run build"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "dashboard/app",
22
+ "README.md",
23
+ "image.png",
24
+ "examples/bgr-startup.sh"
25
+ ],
26
+ "keywords": [
27
+ "process-manager",
28
+ "bun",
29
+ "monitoring",
30
+ "devops",
31
+ "deployment",
32
+ "background",
33
+ "daemon"
34
+ ],
35
+ "author": "7flash",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/7flash/bgrun.git"
40
+ },
41
+ "devDependencies": {
42
+ "@types/bun": "^1.3.10",
43
+ "bun-types": "latest"
44
+ },
45
+ "peerDependencies": {
46
+ "typescript": "^5.0.0"
47
+ },
48
+ "dependencies": {
49
+ "boxen": "^8.0.1",
50
+ "chalk": "^5.4.1",
51
+ "dedent": "^1.5.3",
52
+ "measure-fn": "3.10.1",
53
+ "melina": "2.3.3",
54
+ "react": "^19.2.4",
55
+ "react-dom": "^19.2.4",
56
+ "sqlite-zod-orm": "3.26.1"
57
+ },
58
+ "engines": {
59
+ "bun": ">=1.0.0"
60
+ }
61
+ }
package/src/api.ts CHANGED
@@ -39,7 +39,7 @@ export {
39
39
  ensureDir,
40
40
  getHomeDir,
41
41
  isWindows,
42
- getProcessBatchMemory,
42
+ getProcessBatchResources,
43
43
  getProcessMemory
44
44
  } from './platform'
45
45
 
@@ -51,13 +51,13 @@ export { getVersion, calculateRuntime, parseEnvString, validateDirectory } from
51
51
 
52
52
  // --- Default Export (namespace style) ---
53
53
  import { getAllProcesses, getProcess, insertProcess, removeProcess, removeProcessByName, removeAllProcesses, retryDatabaseOperation, getDbInfo, dbPath, bgrHome } from './db'
54
- import { isProcessRunning, terminateProcess, readFileTail, getProcessPorts, findChildPid, findPidByPort, getShellCommand, killProcessOnPort, waitForPortFree, ensureDir, getHomeDir, isWindows, getProcessBatchMemory, getProcessMemory } from './platform'
54
+ import { isProcessRunning, terminateProcess, readFileTail, getProcessPorts, findChildPid, findPidByPort, getShellCommand, killProcessOnPort, waitForPortFree, ensureDir, getHomeDir, isWindows, getProcessBatchResources, getProcessMemory } from './platform'
55
55
  import { handleRun } from './commands/run'
56
56
  import { getVersion, calculateRuntime, parseEnvString, validateDirectory } from './utils'
57
57
 
58
58
  export default {
59
59
  getAllProcesses, getProcess, insertProcess, removeProcess, removeProcessByName, removeAllProcesses, retryDatabaseOperation, getDbInfo, dbPath, bgrHome,
60
- isProcessRunning, terminateProcess, readFileTail, getProcessPorts, findChildPid, findPidByPort, getShellCommand, killProcessOnPort, waitForPortFree, ensureDir, getHomeDir, isWindows, getProcessBatchMemory, getProcessMemory,
60
+ isProcessRunning, terminateProcess, readFileTail, getProcessPorts, findChildPid, findPidByPort, getShellCommand, killProcessOnPort, waitForPortFree, ensureDir, getHomeDir, isWindows, getProcessBatchResources, getProcessMemory,
61
61
  handleRun,
62
62
  getVersion, calculateRuntime, parseEnvString, validateDirectory,
63
63
  }
@@ -4,7 +4,7 @@ import type { ProcessTableRow } from "../table";
4
4
  import { getAllProcesses } from "../db";
5
5
  import { announce } from "../logger";
6
6
  import { isProcessRunning, calculateRuntime, parseEnvString } from "../utils";
7
- import { getProcessPorts, getProcessBatchMemory } from "../platform";
7
+ import { getProcessPorts, getProcessBatchResources } from "../platform";
8
8
  import { measure } from "measure-fn";
9
9
 
10
10
  function formatMemory(bytes: number): string {
@@ -51,12 +51,12 @@ export async function showAll(opts?: { json?: boolean; filter?: string }) {
51
51
 
52
52
  // Batch fetch memory for all PIDs
53
53
  const allPids = filtered.map(p => p.pid);
54
- const memoryMap = await getProcessBatchMemory(allPids);
54
+ const resourceMap = await getProcessBatchResources(allPids);
55
55
 
56
56
  for (const proc of filtered) {
57
57
  const isRunning = await isProcessRunning(proc.pid, proc.command);
58
58
  const runtime = calculateRuntime(proc.timestamp);
59
- const mem = isRunning ? (memoryMap.get(proc.pid) || 0) : 0;
59
+ const mem = isRunning ? (resourceMap.get(proc.pid)?.memory || 0) : 0;
60
60
 
61
61
  const ports = isRunning ? await getProcessPorts(proc.pid) : [];
62
62
  tableData.push({
@@ -17,6 +17,23 @@ export async function handleRun(options: CommandOptions) {
17
17
 
18
18
  const existingProcess = name ? getProcess(name) : null;
19
19
 
20
+ // Auto-start unmet dependencies before starting this process
21
+ if (name && existingProcess) {
22
+ const { getUnmetDeps } = await import('../deps');
23
+ const unmet = await getUnmetDeps(name);
24
+ if (unmet.length > 0) {
25
+ await run.measure(`Start ${unmet.length} dependencies for "${name}"`, async () => {
26
+ for (const depName of unmet) {
27
+ const depProc = getProcess(depName);
28
+ if (depProc) {
29
+ announce(`📦 Starting dependency "${depName}" for "${name}"`, 'Dependency');
30
+ await handleRun({ action: 'run', name: depName, force: true, remoteName: '' });
31
+ }
32
+ }
33
+ });
34
+ }
35
+ }
36
+
20
37
  if (existingProcess) {
21
38
  const finalDirectory = directory || existingProcess.workdir;
22
39
  validateDirectory(finalDirectory);
package/src/deps.ts ADDED
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Process Dependency Graph
3
+ *
4
+ * Defines and resolves process startup dependencies.
5
+ * Dependencies are stored in process env as BGR_DEPENDS_ON=name1,name2
6
+ *
7
+ * Features:
8
+ * - Topological sort for startup order
9
+ * - Cycle detection
10
+ * - Dependency graph as adjacency list
11
+ * - Auto-start dependencies on process run
12
+ */
13
+
14
+ import { getAllProcesses, getProcess } from './db'
15
+ import { isProcessRunning } from './platform'
16
+ import { parseEnvString } from './utils'
17
+
18
+ export interface DepNode {
19
+ name: string
20
+ dependsOn: string[] // processes this depends ON (must start first)
21
+ dependedBy: string[] // processes that depend on THIS
22
+ running: boolean
23
+ pid: number
24
+ }
25
+
26
+ export interface DepGraph {
27
+ nodes: DepNode[]
28
+ order: string[] // topological sort (startup order)
29
+ hasCycle: boolean
30
+ cycleNodes?: string[] // nodes involved in cycle
31
+ }
32
+
33
+ /** Parse BGR_DEPENDS_ON from a process env string */
34
+ export function getDependencies(envStr: string): string[] {
35
+ const env = parseEnvString(envStr)
36
+ const raw = env.BGR_DEPENDS_ON || ''
37
+ return raw.split(',').map(s => s.trim()).filter(Boolean)
38
+ }
39
+
40
+ /** Build the full dependency graph from all registered processes */
41
+ export async function buildDepGraph(): Promise<DepGraph> {
42
+ const processes = getAllProcesses()
43
+ const nodeMap = new Map<string, DepNode>()
44
+
45
+ // Phase 1: Create all nodes
46
+ for (const proc of processes) {
47
+ const deps = getDependencies(proc.env)
48
+ const alive = await isProcessRunning(proc.pid, proc.command)
49
+ nodeMap.set(proc.name, {
50
+ name: proc.name,
51
+ dependsOn: deps,
52
+ dependedBy: [],
53
+ running: alive,
54
+ pid: proc.pid,
55
+ })
56
+ }
57
+
58
+ // Phase 2: Build reverse edges (dependedBy)
59
+ for (const node of nodeMap.values()) {
60
+ for (const dep of node.dependsOn) {
61
+ const depNode = nodeMap.get(dep)
62
+ if (depNode) {
63
+ depNode.dependedBy.push(node.name)
64
+ }
65
+ }
66
+ }
67
+
68
+ // Phase 3: Topological sort (Kahn's algorithm)
69
+ const inDegree = new Map<string, number>()
70
+ for (const node of nodeMap.values()) {
71
+ inDegree.set(node.name, node.dependsOn.filter(d => nodeMap.has(d)).length)
72
+ }
73
+
74
+ const queue: string[] = []
75
+ for (const [name, degree] of inDegree) {
76
+ if (degree === 0) queue.push(name)
77
+ }
78
+
79
+ const order: string[] = []
80
+ while (queue.length > 0) {
81
+ const current = queue.shift()!
82
+ order.push(current)
83
+
84
+ const node = nodeMap.get(current)!
85
+ for (const dependent of node.dependedBy) {
86
+ const newDegree = (inDegree.get(dependent) || 0) - 1
87
+ inDegree.set(dependent, newDegree)
88
+ if (newDegree === 0) queue.push(dependent)
89
+ }
90
+ }
91
+
92
+ const hasCycle = order.length < nodeMap.size
93
+ const cycleNodes = hasCycle
94
+ ? [...nodeMap.keys()].filter(n => !order.includes(n))
95
+ : undefined
96
+
97
+ return {
98
+ nodes: [...nodeMap.values()],
99
+ order,
100
+ hasCycle,
101
+ cycleNodes,
102
+ }
103
+ }
104
+
105
+ /** Get unmet dependencies for a specific process */
106
+ export async function getUnmetDeps(name: string): Promise<string[]> {
107
+ const proc = getProcess(name)
108
+ if (!proc) return []
109
+
110
+ const deps = getDependencies(proc.env)
111
+ const unmet: string[] = []
112
+
113
+ for (const depName of deps) {
114
+ const depProc = getProcess(depName)
115
+ if (!depProc) {
116
+ unmet.push(depName) // dependency not even registered
117
+ continue
118
+ }
119
+ const alive = await isProcessRunning(depProc.pid, depProc.command)
120
+ if (!alive) {
121
+ unmet.push(depName)
122
+ }
123
+ }
124
+
125
+ return unmet
126
+ }
package/src/guard.ts ADDED
@@ -0,0 +1,157 @@
1
+ /**
2
+ * BGR Standalone Process Guard
3
+ *
4
+ * Runs as an independent process that monitors ALL guarded processes
5
+ * (BGR_KEEP_ALIVE=true) and the dashboard itself. If the dashboard
6
+ * crashes, the guard restarts it. If any guarded process dies, the
7
+ * guard restarts it.
8
+ *
9
+ * This is the "outer shell" — it cannot be killed by a dashboard crash.
10
+ *
11
+ * Usage:
12
+ * bgrun --guard # Start guard as a managed bgrun process
13
+ * bgrun --_guard-loop # (Internal) Actually run the guard loop
14
+ * bgrun --_guard-loop 30 # Check every 30 seconds
15
+ */
16
+
17
+ import { getAllProcesses, getProcess } from './db';
18
+ import { isProcessRunning, getProcessPorts, findChildPid } from './platform';
19
+ import { handleRun } from './commands/run';
20
+ import { parseEnvString } from './utils';
21
+
22
+ const DEFAULT_INTERVAL_MS = 30_000;
23
+ const MAX_BACKOFF_MS = 5 * 60_000; // 5 minutes max
24
+ const CRASH_THRESHOLD = 5; // Start backoff after this many restarts
25
+ const STABILITY_WINDOW_MS = 120_000; // 2 minutes stable = reset counter
26
+
27
+ interface GuardState {
28
+ restartCounts: Map<string, number>;
29
+ nextRestartTime: Map<string, number>;
30
+ lastSeenAlive: Map<string, number>;
31
+ }
32
+
33
+ const state: GuardState = {
34
+ restartCounts: new Map(),
35
+ nextRestartTime: new Map(),
36
+ lastSeenAlive: new Map(),
37
+ };
38
+
39
+ async function restartProcess(name: string): Promise<boolean> {
40
+ try {
41
+ await handleRun({
42
+ action: 'run',
43
+ name,
44
+ force: true,
45
+ remoteName: '',
46
+ });
47
+ return true;
48
+ } catch (err: any) {
49
+ console.error(`[guard] ✗ Failed to restart "${name}": ${err.message}`);
50
+ return false;
51
+ }
52
+ }
53
+
54
+ function getBackoffMs(restartCount: number): number {
55
+ if (restartCount <= CRASH_THRESHOLD) return 0;
56
+ const exponent = restartCount - CRASH_THRESHOLD;
57
+ return Math.min(30_000 * Math.pow(2, exponent - 1), MAX_BACKOFF_MS);
58
+ }
59
+
60
+ async function guardCycle(): Promise<void> {
61
+ try {
62
+ const processes = getAllProcesses();
63
+ if (processes.length === 0) return;
64
+
65
+ const now = Date.now();
66
+ let checked = 0;
67
+ let restarted = 0;
68
+ let skipped = 0;
69
+
70
+ for (const proc of processes) {
71
+ // Skip the guard process itself
72
+ if (proc.name === 'bgr-guard') continue;
73
+
74
+ const env = proc.env ? parseEnvString(proc.env) : {};
75
+ const isGuarded = env.BGR_KEEP_ALIVE === 'true';
76
+ const isDashboard = proc.name === 'bgr-dashboard';
77
+
78
+ // Guard both: explicitly guarded processes AND the dashboard
79
+ if (!isGuarded && !isDashboard) continue;
80
+
81
+ checked++;
82
+
83
+ try {
84
+ const alive = await isProcessRunning(proc.pid, proc.command);
85
+
86
+ if (!alive && proc.pid > 0) {
87
+ // Check backoff
88
+ const nextRestart = state.nextRestartTime.get(proc.name) || 0;
89
+ if (now < nextRestart) {
90
+ const waitSecs = Math.round((nextRestart - now) / 1000);
91
+ skipped++;
92
+ continue;
93
+ }
94
+
95
+ console.log(`[guard] ⚠ "${proc.name}" (PID ${proc.pid}) is dead — restarting...`);
96
+
97
+ const success = await restartProcess(proc.name);
98
+ if (success) {
99
+ const count = (state.restartCounts.get(proc.name) || 0) + 1;
100
+ state.restartCounts.set(proc.name, count);
101
+ state.lastSeenAlive.delete(proc.name);
102
+
103
+ const backoff = getBackoffMs(count);
104
+ if (backoff > 0) {
105
+ state.nextRestartTime.set(proc.name, now + backoff);
106
+ console.log(`[guard] ✓ Restarted "${proc.name}" (#${count}). Crash loop: next check in ${Math.round(backoff / 1000)}s`);
107
+ } else {
108
+ console.log(`[guard] ✓ Restarted "${proc.name}" (#${count})`);
109
+ }
110
+ restarted++;
111
+ }
112
+ } else if (alive) {
113
+ // Track stability — if alive for STABILITY_WINDOW, reset counters
114
+ const count = state.restartCounts.get(proc.name) || 0;
115
+ if (count > 0) {
116
+ const lastSeen = state.lastSeenAlive.get(proc.name);
117
+ if (!lastSeen) {
118
+ state.lastSeenAlive.set(proc.name, now);
119
+ } else if (now - lastSeen > STABILITY_WINDOW_MS) {
120
+ state.restartCounts.delete(proc.name);
121
+ state.nextRestartTime.delete(proc.name);
122
+ state.lastSeenAlive.delete(proc.name);
123
+ console.log(`[guard] ✓ "${proc.name}" stable for ${Math.round(STABILITY_WINDOW_MS / 1000)}s — reset counters`);
124
+ }
125
+ }
126
+ }
127
+ } catch (err: any) {
128
+ console.error(`[guard] Error checking "${proc.name}": ${err.message}`);
129
+ }
130
+ }
131
+
132
+ if (restarted > 0) {
133
+ console.log(`[guard] Cycle: ${checked} checked, ${restarted} restarted, ${skipped} in backoff`);
134
+ }
135
+ } catch (err: any) {
136
+ console.error(`[guard] Error in guard cycle: ${err.message}`);
137
+ }
138
+ }
139
+
140
+ export async function startGuardLoop(intervalMs: number = DEFAULT_INTERVAL_MS) {
141
+ const interval = intervalMs || DEFAULT_INTERVAL_MS;
142
+
143
+ console.log(`[guard] ═══════════════════════════════════════════`);
144
+ console.log(`[guard] 🛡️ BGR Standalone Guard started`);
145
+ console.log(`[guard] Check interval: ${interval / 1000}s`);
146
+ console.log(`[guard] Crash backoff threshold: ${CRASH_THRESHOLD} restarts`);
147
+ console.log(`[guard] Stability window: ${STABILITY_WINDOW_MS / 1000}s`);
148
+ console.log(`[guard] Monitoring: BGR_KEEP_ALIVE=true + bgr-dashboard`);
149
+ console.log(`[guard] Started: ${new Date().toLocaleString()}`);
150
+ console.log(`[guard] ═══════════════════════════════════════════`);
151
+
152
+ // Run initial check immediately
153
+ await guardCycle();
154
+
155
+ // Then run on interval
156
+ setInterval(guardCycle, interval);
157
+ }
package/src/index.ts CHANGED
@@ -12,7 +12,7 @@ import type { CommandOptions } from "./types";
12
12
  import { error, announce } from "./logger";
13
13
  // startServer is dynamically imported only when --_serve is used
14
14
  // to avoid loading melina (which has side-effects) on every bgrun command
15
- import { getHomeDir, getShellCommand, findChildPid, isProcessRunning, terminateProcess, getProcessPorts, killProcessOnPort, waitForPortFree } from "./platform";
15
+ import { getHomeDir, getShellCommand, findChildPid, isProcessRunning, terminateProcess, getProcessPorts, killProcessOnPort, waitForPortFree, isPortFree } from "./platform";
16
16
  import { insertProcess, removeProcessByName, getProcess, retryDatabaseOperation, getDbInfo } from "./db";
17
17
  import dedent from "dedent";
18
18
  import chalk from "chalk";
@@ -38,6 +38,7 @@ async function showHelp() {
38
38
  bgrun List all processes
39
39
  bgrun [name] Show details for a process
40
40
  bgrun --dashboard Launch web dashboard (managed by bgrun)
41
+ bgrun --guard Launch standalone process guard
41
42
  bgrun --restart [name] Restart a process
42
43
  bgrun --restart-all Restart ALL registered processes
43
44
  bgrun --stop [name] Stop a process (keep in registry)
@@ -105,8 +106,10 @@ async function run() {
105
106
  stdout: { type: 'string' },
106
107
  stderr: { type: 'string' },
107
108
  dashboard: { type: 'boolean' },
109
+ guard: { type: 'boolean' },
108
110
  debug: { type: 'boolean' },
109
111
  "_serve": { type: 'boolean' },
112
+ "_guard-loop": { type: 'boolean' },
110
113
  port: { type: 'string' },
111
114
  },
112
115
  strict: false,
@@ -122,6 +125,15 @@ async function run() {
122
125
  return;
123
126
  }
124
127
 
128
+ // Internal: actually run the guard loop (spawned by --guard)
129
+ if (values['_guard-loop']) {
130
+ const { startGuardLoop } = await import("./guard");
131
+ const intervalStr = positionals[0];
132
+ const intervalMs = intervalStr ? parseInt(intervalStr) * 1000 : undefined;
133
+ await startGuardLoop(intervalMs);
134
+ return;
135
+ }
136
+
125
137
  // Dashboard: spawn the dashboard server as a bgr-managed process
126
138
  if (values.dashboard) {
127
139
  const dashboardName = 'bgr-dashboard';
@@ -186,6 +198,21 @@ async function run() {
186
198
  spawnEnv.BUN_PORT = requestedPort;
187
199
  }
188
200
 
201
+ // Resolve the target port: --port flag > BUN_PORT env > default 3000
202
+ const targetPort = parseInt(requestedPort || Bun.env.BUN_PORT || '3000');
203
+ if (!isNaN(targetPort) && targetPort > 0) {
204
+ // Auto-kill whatever occupies the target port so dashboard always reclaims it
205
+ const portFree = await isPortFree(targetPort);
206
+ if (!portFree) {
207
+ console.log(chalk.yellow(` ⚡ Port ${targetPort} is occupied — reclaiming...`));
208
+ await killProcessOnPort(targetPort);
209
+ const freed = await waitForPortFree(targetPort, 5000);
210
+ if (!freed) {
211
+ console.log(chalk.red(` ⚠ Could not free port ${targetPort} — dashboard may pick a fallback port`));
212
+ }
213
+ }
214
+ }
215
+
189
216
  const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
190
217
  env: spawnEnv,
191
218
  cwd: bgrDir,
@@ -246,6 +273,85 @@ async function run() {
246
273
  return;
247
274
  }
248
275
 
276
+ // Guard: spawn the standalone guard as a bgr-managed process
277
+ if (values.guard) {
278
+ const guardName = 'bgr-guard';
279
+ const homePath = getHomeDir();
280
+ const bgrDir = join(homePath, '.bgr');
281
+
282
+ // Check if guard is already running
283
+ const existing = getProcess(guardName);
284
+ if (existing && await isProcessRunning(existing.pid)) {
285
+ announce(
286
+ `Guard is already running (PID ${existing.pid})\n\n` +
287
+ ` Use ${chalk.yellow(`bgrun --stop ${guardName}`)} to stop it\n` +
288
+ ` Use ${chalk.yellow(`bgrun --guard --force`)} to restart`,
289
+ 'BGR Guard'
290
+ );
291
+ return;
292
+ }
293
+
294
+ // Kill existing if force
295
+ if (existing) {
296
+ if (await isProcessRunning(existing.pid)) {
297
+ await terminateProcess(existing.pid);
298
+ }
299
+ await retryDatabaseOperation(() => removeProcessByName(guardName));
300
+ }
301
+
302
+ const { resolve } = require('path');
303
+ const scriptPath = resolve(process.argv[1]);
304
+ const spawnCommand = `bun run ${scriptPath} --_guard-loop`;
305
+ const command = `bgrun --_guard-loop`;
306
+ const stdoutPath = join(bgrDir, `${guardName}-out.txt`);
307
+ const stderrPath = join(bgrDir, `${guardName}-err.txt`);
308
+
309
+ await Bun.write(stdoutPath, '');
310
+ await Bun.write(stderrPath, '');
311
+
312
+ const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
313
+ env: { ...Bun.env },
314
+ cwd: bgrDir,
315
+ stdout: Bun.file(stdoutPath),
316
+ stderr: Bun.file(stderrPath),
317
+ });
318
+
319
+ newProcess.unref();
320
+ await sleep(1000);
321
+ const actualPid = await findChildPid(newProcess.pid);
322
+
323
+ await retryDatabaseOperation(() =>
324
+ insertProcess({
325
+ pid: actualPid,
326
+ workdir: bgrDir,
327
+ command,
328
+ name: guardName,
329
+ env: 'BGR_KEEP_ALIVE=false', // Guard doesn't guard itself
330
+ configPath: '',
331
+ stdout_path: stdoutPath,
332
+ stderr_path: stderrPath,
333
+ })
334
+ );
335
+
336
+ const msg = dedent`
337
+ ${chalk.bold('🛡️ BGR Standalone Guard launched')}
338
+ ${chalk.gray('─'.repeat(40))}
339
+
340
+ Monitors: All processes with BGR_KEEP_ALIVE=true
341
+ Also watches: bgr-dashboard (auto-restart if it dies)
342
+ Check interval: 30 seconds
343
+ Backoff: Exponential after 5 rapid crashes
344
+
345
+ ${chalk.gray('─'.repeat(40))}
346
+ Process: ${chalk.white(guardName)} | PID: ${chalk.white(String(actualPid))}
347
+
348
+ ${chalk.yellow(`bgrun ${guardName} --logs`)} View guard logs
349
+ ${chalk.yellow(`bgrun --stop ${guardName}`)} Stop the guard
350
+ `;
351
+ announce(msg, 'BGR Guard');
352
+ return;
353
+ }
354
+
249
355
  if (values.version) {
250
356
  console.log(`bgrun version: ${await getVersion()}`);
251
357
  return;
@@ -435,6 +541,5 @@ async function run() {
435
541
  }
436
542
 
437
543
  run().catch(err => {
438
- console.error(chalk.red(err));
439
- process.exit(1);
544
+ error(err);
440
545
  });