bgrun 3.12.16 → 3.12.22

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 CHANGED
@@ -12,8 +12,8 @@
12
12
  Start, stop, restart, and monitor any process — from dev servers to Docker containers.
13
13
  Zero config. One command. Beautiful dashboard included.
14
14
 
15
- ```
16
- bun install -g bgrun
15
+ ```bash
16
+ bunx bgrun --help
17
17
  ```
18
18
 
19
19
  </div>
@@ -35,24 +35,36 @@ bun install -g bgrun
35
35
  | Programmatic API | ✅ | ✅ (first-class TypeScript) |
36
36
  | Process persistence | ✅ | ✅ (SQLite) |
37
37
 
38
- > **Note:** The CLI is available as both `bgrun` and `bgr` (alias). All examples below use `bgrun`.
38
+ > **Note:** Prefer `bunx bgrun`. Bare `bgrun` only works if you installed it globally or already have a matching shim on `PATH`.
39
39
 
40
40
  ---
41
41
 
42
42
  ## Quick Start
43
43
 
44
44
  ```bash
45
- # Install globally
46
- bun install -g bgrun
45
+ # Run without a global install
46
+ bunx bgrun --help
47
+
48
+ # Skip automatic .config.toml loading for one-off commands
49
+ bunx bgrun --no-config -- bun run script.ts
50
+
51
+ # Start a process
52
+ bunx bgrun --name my-api --directory ./my-project --command "bun run server.ts"
53
+
54
+ # Start a managed process with an auto-generated date name
55
+ bunx bgrun -- bun run server.ts
47
56
 
48
- # Start a process
49
- bgrun --name my-api --directory ./my-project --command "bun run server.ts"
57
+ # Run in the current terminal with config env loaded
58
+ bunx bgrun inline -- bun run dev
59
+
60
+ # Export config env into your current shell
61
+ Invoke-Expression (bunx bgrun envit)
50
62
 
51
63
  # List all processes
52
- bgrun
64
+ bunx bgrun
53
65
 
54
66
  # Open the web dashboard
55
- bgrun --dashboard
67
+ bunx bgrun --dashboard
56
68
  ```
57
69
 
58
70
  That's it. bgrun tracks the PID, captures stdout/stderr, detects the port, and survives terminal close.
@@ -62,7 +74,7 @@ That's it. bgrun tracks the PID, captures stdout/stderr, detects the port, and s
62
74
  ## 📊 Web Dashboard
63
75
 
64
76
 
65
- Launch with `bgrun --dashboard` and open `http://localhost:3001`. Processes are auto-grouped by working directory.
77
+ Launch with `bunx bgrun --dashboard` and open `http://localhost:3001`. Processes are auto-grouped by working directory.
66
78
 
67
79
  **Expose with Caddy** for remote access:
68
80
 
@@ -117,6 +129,14 @@ bgrun --name my-api \
117
129
  --command "bun run server.ts"
118
130
  ```
119
131
 
132
+ Anonymous managed start:
133
+
134
+ ```bash
135
+ bgrun -- bun run server.ts
136
+ ```
137
+
138
+ This generates a date-based name such as `april-fifth`. If that name already exists, bgrun appends `-hhmmss`.
139
+
120
140
  Short form — if you're already *in* the project directory:
121
141
 
122
142
  ```bash
@@ -403,14 +423,33 @@ The convention: `[section]` becomes the prefix, `key` becomes the suffix, joined
403
423
 
404
424
  If no `--config` is specified, bgrun looks for `.config.toml` in the working directory automatically.
405
425
 
406
- ---
426
+ ### Loading config env without managing the process
427
+
428
+ Run a command in the current terminal with config env applied:
429
+
430
+ ```bash
431
+ bgrun inline -- bun run dev
432
+ ```
407
433
 
408
- ## Programmatic API
434
+ Print shell commands that export config values into your current shell:
409
435
 
410
- bgrun exposes its internals as importable TypeScript functions:
436
+ ```powershell
437
+ Invoke-Expression (bgrun envit)
438
+ ```
439
+
440
+ ```bash
441
+ eval "$(bgrun envit --shell sh)"
442
+ ```
443
+
444
+ ---
411
445
 
412
- > **Packaging note:** the CLI ships from `dist/index.js`, the Bun programmatic API resolves through `dist/api.js`, and the dashboard backend uses built `dist/*` runtime artifacts via `dashboard/lib/runtime.ts`.
413
- > Published packages are now `dist`-first; repository `src/` files remain the development/build source of truth and are not part of the runtime package surface.
446
+ ## Programmatic API
447
+
448
+ bgrun exposes its internals as importable TypeScript functions:
449
+
450
+ > **Packaging note:** the CLI ships from `dist/index.js`, the Bun programmatic API resolves through `dist/api.js`, and the dashboard backend uses built `dist/*` runtime artifacts via `dashboard/lib/runtime.ts`.
451
+ > Published packages are now `dist`-first; repository `src/` files remain the development/build source of truth and are not part of the runtime package surface.
452
+ > **Build hooks:** `npm`/`bun` runs the package `prepare` script on local install from the repo and before packing/publishing, so `bun run build` is triggered to refresh `dist/`. `prepublishOnly` also runs `bun run build` right before `npm publish`.
414
453
 
415
454
  ```bash
416
455
  bun add bgrun
@@ -1,11 +1,8 @@
1
1
  /**
2
- * POST /api/guard — Toggle BGR_KEEP_ALIVE for a process
2
+ * POST /api/guard — Toggle per-process watcher guard
3
3
  * Body: { name: string, enabled: boolean }
4
- *
5
- * When enabled=true, the built-in guard will auto-restart this process if it dies.
6
- * When enabled=false, the process is left alone.
7
4
  */
8
- import { getProcess, updateProcessEnv, addHistoryEntry } from '../../../lib/runtime';
5
+ import { getProcess, updateProcessEnv, addHistoryEntry, parseEnvString, stringifyEnvString, syncProcessWatcher } from '../../../lib/runtime';
9
6
 
10
7
  export async function POST(req: Request) {
11
8
  try {
@@ -20,22 +17,17 @@ export async function POST(req: Request) {
20
17
  }
21
18
 
22
19
  // Parse existing env
23
- let env: Record<string, string> = {};
24
- if (proc.env) {
25
- try { env = JSON.parse(proc.env); } catch { env = {}; }
26
- }
20
+ const env = parseEnvString(proc.env || '');
27
21
 
28
- // Toggle BGR_KEEP_ALIVE
29
22
  if (body.enabled) {
30
23
  env.BGR_KEEP_ALIVE = 'true';
31
24
  } else {
32
25
  delete env.BGR_KEEP_ALIVE;
33
26
  }
34
27
 
35
- // Save back
36
- updateProcessEnv(body.name, JSON.stringify(env));
28
+ updateProcessEnv(body.name, stringifyEnvString(env));
29
+ await syncProcessWatcher(body.name, env);
37
30
 
38
- // Record history
39
31
  addHistoryEntry(body.name, body.enabled ? 'guard_on' : 'guard_off');
40
32
 
41
33
  return Response.json({
@@ -1,13 +1,8 @@
1
1
  /**
2
- * POST /api/guard-all — Bulk toggle guard for all processes
2
+ * POST /api/guard-all — Bulk toggle per-process watcher guard
3
3
  * Body: { enabled: boolean }
4
- *
5
- * When enabled=true, sets BGR_KEEP_ALIVE=true for ALL processes (except bgr-dashboard).
6
- * When enabled=false, removes BGR_KEEP_ALIVE from ALL processes.
7
4
  */
8
- import { getAllProcesses, getProcess, updateProcessEnv } from '../../../lib/runtime';
9
-
10
- const SKIP = new Set(['bgr-dashboard']);
5
+ import { getAllProcesses, updateProcessEnv, parseEnvString, stringifyEnvString, syncProcessWatcher, isInternalProcessName } from '../../../lib/runtime';
11
6
 
12
7
  export async function POST(req: Request) {
13
8
  try {
@@ -16,13 +11,9 @@ export async function POST(req: Request) {
16
11
  let count = 0;
17
12
 
18
13
  for (const proc of processes) {
19
- if (SKIP.has(proc.name)) continue;
14
+ if (isInternalProcessName(proc.name)) continue;
20
15
 
21
- // Parse existing env
22
- let env: Record<string, string> = {};
23
- if (proc.env) {
24
- try { env = JSON.parse(proc.env); } catch { env = {}; }
25
- }
16
+ const env = parseEnvString(proc.env || '');
26
17
 
27
18
  const alreadyGuarded = env.BGR_KEEP_ALIVE === 'true';
28
19
  if (body.enabled && alreadyGuarded) continue;
@@ -34,7 +25,8 @@ export async function POST(req: Request) {
34
25
  delete env.BGR_KEEP_ALIVE;
35
26
  }
36
27
 
37
- updateProcessEnv(proc.name, JSON.stringify(env));
28
+ updateProcessEnv(proc.name, stringifyEnvString(env));
29
+ await syncProcessWatcher(proc.name, env);
38
30
  count++;
39
31
  }
40
32
 
@@ -1,5 +1,5 @@
1
- import { guardEvents } from '../../../lib/runtime';
1
+ import { getRecentGuardEvents } from '../../../lib/runtime';
2
2
 
3
3
  export async function GET() {
4
- return Response.json(guardEvents);
5
- }
4
+ return Response.json(getRecentGuardEvents());
5
+ }
@@ -4,7 +4,7 @@
4
4
  * Scans existing processes' env for PORT= values,
5
5
  * then returns the next unused port starting from a base (default 3001).
6
6
  */
7
- import { getAllProcesses } from '../../../lib/runtime';
7
+ import { getAllProcesses, parseCommandEnv } from '../../../lib/runtime';
8
8
 
9
9
  export async function GET(req: Request) {
10
10
  const url = new URL(req.url);
@@ -14,19 +14,39 @@ export async function GET(req: Request) {
14
14
  const usedPorts = new Set<number>();
15
15
 
16
16
  for (const proc of processes) {
17
- // Parse PORT from env string (comma-separated KEY=VAL)
17
+ // Parse PORT/BUN_PORT from both stored env and inline command env.
18
18
  const envStr = proc.env || '';
19
- const portMatch = envStr.match(/(?:^|,)PORT=(\d+)/);
20
- if (portMatch) {
21
- usedPorts.add(parseInt(portMatch[1]));
19
+ const storedPortMatch = envStr.match(/(?:^|,)(?:PORT|BUN_PORT)=(\d+)/);
20
+ if (storedPortMatch) {
21
+ usedPorts.add(parseInt(storedPortMatch[1]));
22
+ }
23
+
24
+ const commandEnv = parseCommandEnv(proc.command || '');
25
+ const commandPort = parseInt(commandEnv.PORT || commandEnv.BUN_PORT || '', 10);
26
+ if (!isNaN(commandPort) && commandPort > 0) {
27
+ usedPorts.add(commandPort);
22
28
  }
23
29
  }
24
30
 
25
- // Find next available port
31
+ // Find next available port, skipping both registered and actually-bound ports
26
32
  let nextPort = base;
27
- while (usedPorts.has(nextPort)) {
33
+ while (usedPorts.has(nextPort) || await isPortInUse(nextPort)) {
28
34
  nextPort++;
29
35
  }
30
36
 
31
37
  return Response.json({ port: nextPort, usedPorts: Array.from(usedPorts).sort((a, b) => a - b) });
32
38
  }
39
+
40
+ async function isPortInUse(port: number): Promise<boolean> {
41
+ try {
42
+ const server = Bun.serve({
43
+ port,
44
+ hostname: '127.0.0.1',
45
+ fetch() { return new Response(''); },
46
+ });
47
+ server.stop(true);
48
+ return false;
49
+ } catch {
50
+ return true;
51
+ }
52
+ }
@@ -1,7 +1,5 @@
1
- import { getAllProcesses, updateProcessPid } from '../../../lib/runtime';
2
- import { calculateRuntime } from '../../../lib/runtime';
3
- import { getProcessBatchResources, reconcileProcessPids } from '../../../lib/runtime';
4
- import { guardRestartCounts } from '../../../lib/runtime';
1
+ import { getAllProcesses, updateProcessPid, calculateRuntime, getGuardRestartCounts, isInternalProcessName } from '../../../lib/runtime';
2
+ import { getProcessBatchResources, reconcileProcessPids, resolvePidWithPorts, getProcessPorts, findChildPid } from '../../../../dist/api.js';
5
3
  import { measure, createMeasure } from 'measure-fn';
6
4
  import { $ } from 'bun';
7
5
 
@@ -111,8 +109,9 @@ function getProcessGroup(envStr: string): string | null {
111
109
 
112
110
  async function fetchProcesses(): Promise<any[]> {
113
111
  return await api.measure('Fetch processes', async (m) => {
114
- const procs = getAllProcesses();
112
+ const procs = getAllProcesses().filter((p: any) => !isInternalProcessName(p.name));
115
113
  const pids = procs.map((p: any) => p.pid);
114
+ const guardRestartCounts = getGuardRestartCounts();
116
115
 
117
116
  // Three subprocess calls total (not 3×N)
118
117
  let [runningPids, portMap, resourceMap] = await Promise.all([
@@ -160,6 +159,33 @@ async function fetchProcesses(): Promise<any[]> {
160
159
  const now = Date.now();
161
160
  const isWin = process.platform === 'win32';
162
161
 
162
+ for (const p of procs) {
163
+ const running = runningPids?.has(p.pid) ?? false;
164
+ const ports = running ? (portMap?.get(p.pid) || []) : [];
165
+ if (!running || ports.length > 0) continue;
166
+
167
+ const resolved = await m(`Resolve live wrapper PID for ${p.name}`, () =>
168
+ withTimeout(resolvePidWithPorts(p.pid), { pid: p.pid, ports: [] })
169
+ );
170
+ if (resolved.pid !== p.pid && resolved.ports.length > 0) {
171
+ const oldPid = p.pid;
172
+ p.pid = resolved.pid;
173
+ updateProcessPid(p.name, resolved.pid);
174
+ runningPids?.delete(oldPid);
175
+ runningPids?.add(resolved.pid);
176
+ portMap?.set(resolved.pid, resolved.ports);
177
+
178
+ const refreshedResource = await withTimeout(
179
+ getProcessBatchResources([resolved.pid]),
180
+ new Map<number, { memory: number, cpu: number }>()
181
+ );
182
+ const nextResource = refreshedResource.get(resolved.pid);
183
+ if (nextResource) {
184
+ resourceMap?.set(resolved.pid, nextResource);
185
+ }
186
+ }
187
+ }
188
+
163
189
  return procs.map((p: any) => {
164
190
  const running = runningPids?.has(p.pid) ?? false;
165
191
  const ports = running ? (portMap?.get(p.pid) || []) : [];
@@ -744,7 +744,7 @@ export default function mount(): () => void {
744
744
  if (!btn || !label) return;
745
745
 
746
746
  const targetCount = allProcesses.filter(p => {
747
- if (p.name === 'bgr-dashboard' || p.name === 'bgr-guard') return false;
747
+ if (p.name === 'bgr-dashboard') return false;
748
748
  if (groupQuery && p.group !== groupQuery) return false;
749
749
  return true;
750
750
  }).length;
@@ -806,23 +806,6 @@ export default function mount(): () => void {
806
806
  guardAllBtn.title = allGuarded ? 'Remove guard from all processes' : 'Guard all processes (auto-restart on crash)';
807
807
  }
808
808
 
809
- // Update guard sentinel pill
810
- const guardPill = $('guard-sentinel-pill');
811
- const guardLabel = $('guard-sentinel-label');
812
- if (guardPill && guardLabel) {
813
- const guardProc = processes.find(p => p.name === 'bgr-guard');
814
- guardPill.classList.remove('active', 'stopped');
815
- if (guardProc && guardProc.running) {
816
- guardPill.classList.add('active');
817
- const restarts = guardProc.guardRestarts || 0;
818
- guardLabel.textContent = restarts > 0 ? `Guard: ON (${restarts}↻)` : 'Guard: ON';
819
- } else if (guardProc) {
820
- guardPill.classList.add('stopped');
821
- guardLabel.textContent = 'Guard: OFF';
822
- } else {
823
- guardLabel.textContent = 'Guard: –';
824
- }
825
- }
826
809
  }
827
810
 
828
811
  function toggleGroup(groupDir: string) {
@@ -3428,7 +3411,7 @@ export default function mount(): () => void {
3428
3411
  if (!deployAllBtn || deployAllBtn.disabled) return;
3429
3412
 
3430
3413
  const targets = allProcesses.filter(p => {
3431
- if (p.name === 'bgr-dashboard' || p.name === 'bgr-guard') return false;
3414
+ if (p.name === 'bgr-dashboard') return false;
3432
3415
  if (groupQuery && p.group !== groupQuery) return false;
3433
3416
  return true;
3434
3417
  });
@@ -50,7 +50,7 @@ export default function DashboardPage() {
50
50
  {/* Guard Activity Feed */}
51
51
  <div className="guard-activity" id="guard-activity">
52
52
  <div className="guard-activity-header">
53
- <span className="guard-activity-title">🛡️ Guard Activity</span>
53
+ <span className="guard-activity-title">🛡️ Watcher Activity</span>
54
54
  <span className="guard-activity-empty" id="guard-activity-empty">No recent activity</span>
55
55
  </div>
56
56
  <div className="guard-activity-list" id="guard-activity-list"></div>
@@ -122,10 +122,6 @@ export default function DashboardPage() {
122
122
  </svg>
123
123
  </button>
124
124
  </div>
125
- <span className="guard-sentinel-pill" id="guard-sentinel-pill" title="Standalone guard process status">
126
- <span className="guard-sentinel-dot" id="guard-sentinel-dot" />
127
- <span id="guard-sentinel-label">Guard: –</span>
128
- </span>
129
125
  <button className="btn btn-ghost btn-icon" id="theme-toggle-btn" title="Toggle light/dark theme">
130
126
  <span id="theme-toggle-icon" style={{ fontSize: '0.85rem' }}>🌙</span>
131
127
  </button>
@@ -1,49 +1,58 @@
1
- export {
2
- db,
3
- getAllProcesses,
4
- getProcess,
5
- insertProcess,
6
- removeProcess,
7
- removeProcessByName,
8
- removeAllProcesses,
9
- updateProcessPid,
10
- updateProcessEnv,
11
- getAllTemplates,
12
- saveTemplate,
13
- deleteTemplate,
14
- getProcessHistory,
15
- getRecentHistory,
16
- addHistoryEntry,
17
- getDependencyGraph,
18
- addDependency,
19
- removeDependency,
20
- getStartOrder,
21
- getDbInfo,
22
- dbPath,
23
- bgrHome,
24
- isProcessRunning,
25
- terminateProcess,
26
- readFileTail,
27
- getProcessPorts,
28
- findChildPid,
29
- findPidByPort,
30
- getShellCommand,
31
- killProcessOnPort,
32
- waitForPortFree,
33
- ensureDir,
34
- getHomeDir,
35
- isWindows,
36
- getProcessBatchResources,
37
- getProcessMemory,
38
- reconcileProcessPids,
39
- handleRun,
40
- getVersion,
41
- calculateRuntime,
42
- parseEnvString,
43
- validateDirectory,
44
- } from '../../dist/api.js'
45
-
46
- export { deployProcess, deployAllProcesses } from '../../dist/deploy.js'
47
- export { buildDepGraph } from '../../dist/deps.js'
48
- export { rotateAllLogs } from '../../dist/log-rotation.js'
49
- export { guardEvents, guardRestartCounts } from '../../dist/server.js'
1
+ export {
2
+ db,
3
+ getAllProcesses,
4
+ getProcess,
5
+ insertProcess,
6
+ removeProcess,
7
+ removeProcessByName,
8
+ removeAllProcesses,
9
+ updateProcessPid,
10
+ updateProcessEnv,
11
+ getAllTemplates,
12
+ saveTemplate,
13
+ deleteTemplate,
14
+ getProcessHistory,
15
+ getRecentHistory,
16
+ addHistoryEntry,
17
+ getDependencyGraph,
18
+ addDependency,
19
+ removeDependency,
20
+ getStartOrder,
21
+ getDbInfo,
22
+ dbPath,
23
+ bgrHome,
24
+ isProcessRunning,
25
+ terminateProcess,
26
+ readFileTail,
27
+ getProcessPorts,
28
+ findChildPid,
29
+ findPidByPort,
30
+ resolvePidWithPorts,
31
+ getShellCommand,
32
+ killProcessOnPort,
33
+ waitForPortFree,
34
+ ensureDir,
35
+ getHomeDir,
36
+ isWindows,
37
+ getProcessBatchResources,
38
+ getProcessMemory,
39
+ reconcileProcessPids,
40
+ handleRun,
41
+ ensureProcessWatcher,
42
+ stopProcessWatcher,
43
+ syncProcessWatcher,
44
+ getGuardRestartCounts,
45
+ getRecentGuardEvents,
46
+ getVersion,
47
+ calculateRuntime,
48
+ parseEnvString,
49
+ stringifyEnvString,
50
+ validateDirectory,
51
+ } from '../../dist/api.js'
52
+
53
+ export { deployProcess, deployAllProcesses } from '../../dist/deploy.js'
54
+ export { buildDepGraph } from '../../dist/deps.js'
55
+ export { rotateAllLogs } from '../../dist/log-rotation.js'
56
+
57
+ export const guardEvents: any[] = []
58
+ export const guardRestartCounts = new Map<string, number>()