bgrun 3.7.0 → 3.8.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/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  <div align="center">
2
2
 
3
- # bgrun
3
+ <img src="./image.png" alt="bgrun" width="600" />
4
4
 
5
- **Bun Background Runner a modern process manager built on Bun**
5
+ **Production-ready process manager with dashboard and programmatic API, designed for running your containers, services, and AI agents.**
6
6
 
7
7
  [![npm](https://img.shields.io/npm/v/bgrun?color=F7A41D&label=npm&logo=npm)](https://www.npmjs.com/package/bgrun)
8
8
  [![bun](https://img.shields.io/badge/runtime-bun-F7A41D?logo=bun)](https://bun.sh/)
@@ -58,6 +58,29 @@ That's it. bgrun tracks the PID, captures stdout/stderr, detects the port, and s
58
58
 
59
59
  ---
60
60
 
61
+ ## 📊 Web Dashboard
62
+
63
+
64
+ Launch with `bgrun --dashboard` and open `http://localhost:3001`. Processes are auto-grouped by working directory.
65
+
66
+ **Expose with Caddy** for remote access:
67
+
68
+ ```
69
+ bgrun.yourdomain.com {
70
+ reverse_proxy localhost:3001
71
+ }
72
+ ```
73
+
74
+ Features:
75
+ - Real-time process status via SSE (no polling)
76
+ - Start, stop, restart, and delete processes from the UI
77
+ - Live stdout/stderr log viewer with search
78
+ - Memory, PID, port, and runtime at a glance
79
+ - Responsive mobile layout
80
+ - Collapsible directory groups
81
+
82
+ ---
83
+
61
84
  ## Table of Contents
62
85
 
63
86
  - [Core Commands](#core-commands)
@@ -697,6 +720,6 @@ MIT
697
720
 
698
721
  <div align="center">
699
722
 
700
- Built by [Mements](https://github.com/Mements) with ⚡ Bun
723
+ Built with ⚡ Bun
701
724
 
702
725
  </div>
@@ -0,0 +1,58 @@
1
+ /**
2
+ * POST /api/deploy/:name — Git pull + install deps + restart a process
3
+ *
4
+ * Only works if the process directory is a git repository.
5
+ * Steps: git pull → bun install → force restart
6
+ */
7
+ import { getProcess } from '../../../../../src/db';
8
+ import { handleRun } from '../../../../../src/commands/run';
9
+ import { measure } from 'measure-fn';
10
+ import { $ } from 'bun';
11
+
12
+ export async function POST(req: Request, { params }: { params: { name: string } }) {
13
+ const name = decodeURIComponent(params.name);
14
+
15
+ try {
16
+ const proc = getProcess(name);
17
+ if (!proc) {
18
+ return Response.json({ error: `Process '${name}' not found` }, { status: 404 });
19
+ }
20
+
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 });
55
+ } catch (e: any) {
56
+ return Response.json({ error: e.message }, { status: 500 });
57
+ }
58
+ }
@@ -652,9 +652,14 @@ table {
652
652
  }
653
653
 
654
654
  .group-header td {
655
- padding: 0.875rem 1.25rem 0.5rem 1.25rem !important;
656
- background: transparent !important;
657
- border: none !important;
655
+ padding: 0.625rem 1.25rem 0.375rem 1.25rem !important;
656
+ background: rgba(255, 255, 255, 0.015) !important;
657
+ border-bottom: 1px solid var(--border-subtle) !important;
658
+ border-top: 1px solid var(--border-glass) !important;
659
+ }
660
+
661
+ .group-header:first-child td {
662
+ border-top: none !important;
658
663
  }
659
664
 
660
665
  .group-header:hover td {
@@ -664,20 +669,18 @@ table {
664
669
  .group-label {
665
670
  display: flex;
666
671
  align-items: center;
667
- gap: 0.5rem;
668
- font-size: 0.75rem;
669
- font-weight: 700;
670
- text-transform: uppercase;
671
- letter-spacing: 0.05em;
672
- color: var(--text-muted);
672
+ gap: 0.375rem;
673
+ font-size: 0.6875rem;
674
+ font-weight: 600;
675
+ color: var(--text-dim);
673
676
  }
674
677
 
675
678
  .group-chevron {
676
- width: 14px;
677
- height: 14px;
679
+ width: 12px;
680
+ height: 12px;
678
681
  stroke: var(--text-dim);
679
682
  fill: none;
680
- stroke-width: 2;
683
+ stroke-width: 2.5;
681
684
  transition: transform var(--transition-fast);
682
685
  transform: rotate(90deg);
683
686
  flex-shrink: 0;
@@ -687,28 +690,21 @@ table {
687
690
  transform: rotate(0deg);
688
691
  }
689
692
 
690
- .group-icon {
691
- font-size: 0.875rem;
692
- }
693
-
694
693
  .group-name {
695
- color: var(--text-secondary);
696
- font-size: 0.7rem;
694
+ color: var(--text-muted);
695
+ font-size: 0.6875rem;
697
696
  font-family: var(--font-mono);
698
- letter-spacing: 0.02em;
697
+ letter-spacing: 0.01em;
699
698
  font-weight: 500;
700
- text-transform: none;
701
699
  }
702
700
 
703
701
  .group-counts {
704
702
  display: flex;
705
703
  align-items: center;
706
- gap: 0.375rem;
707
- margin-left: 0.25rem;
708
- font-size: 0.65rem;
704
+ gap: 0.25rem;
705
+ margin-left: auto;
706
+ font-size: 0.625rem;
709
707
  font-weight: 500;
710
- text-transform: none;
711
- letter-spacing: normal;
712
708
  }
713
709
 
714
710
  .group-count-running {
@@ -716,11 +712,11 @@ table {
716
712
  }
717
713
 
718
714
  .group-count-running.has-running {
719
- color: var(--success);
715
+ color: var(--success-soft);
720
716
  }
721
717
 
722
718
  .group-count-sep {
723
- color: var(--text-dim);
719
+ color: var(--border-hover);
724
720
  }
725
721
 
726
722
  .group-count-total {
@@ -916,6 +912,7 @@ tr.animate-in:nth-child(10) {
916
912
  color: var(--text-muted);
917
913
  font-size: 0.8125rem;
918
914
  font-family: var(--font-mono);
915
+ white-space: nowrap;
919
916
  }
920
917
 
921
918
  /* ─── Action Buttons (SVG-based) ─── */
@@ -986,6 +983,12 @@ tr.animate-in:nth-child(10) {
986
983
  border-color: var(--warning-border);
987
984
  }
988
985
 
986
+ .action-btn.deploy:hover {
987
+ background: rgba(20, 184, 166, 0.1);
988
+ color: #14b8a6;
989
+ border-color: rgba(20, 184, 166, 0.3);
990
+ }
991
+
989
992
  /* ─── Empty State ─── */
990
993
  .empty-state {
991
994
  text-align: center;
@@ -13,15 +13,14 @@ import { render as melinaRender, createElement as h, setReconciler } from 'melin
13
13
 
14
14
  interface ProcessData {
15
15
  name: string;
16
- command: string;
17
- directory: string;
18
16
  pid: number;
19
17
  running: boolean;
20
- port: number | null;
21
- ports: number[];
22
- memory: number; // bytes
23
- group: string | null;
24
- runtime: string;
18
+ port: string;
19
+ command: string;
20
+ memory: number;
21
+ runtime: number;
22
+ directory: string;
23
+ group?: string;
25
24
  timestamp: string;
26
25
  env: string;
27
26
  configPath: string;
@@ -84,6 +83,17 @@ function RestartIcon() {
84
83
  );
85
84
  }
86
85
 
86
+ function DeployIcon() {
87
+ return (
88
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
89
+ <path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z" />
90
+ <path d="M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" />
91
+ <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" />
92
+ <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" />
93
+ </svg>
94
+ );
95
+ }
96
+
87
97
  // ─── Utility: Format Runtime ───
88
98
 
89
99
  function formatRuntime(raw: string): string {
@@ -124,6 +134,17 @@ function formatTimeAgo(date: Date): string {
124
134
  return `${days}d ago`;
125
135
  }
126
136
 
137
+ // ─── Helpers ───
138
+
139
+ function shortenPath(dir: string): string {
140
+ if (!dir) return '';
141
+ const normalized = dir.replace(/\\/g, '/');
142
+ const parts = normalized.split('/');
143
+ // Show last 2 segments (e.g. "Code/bgr" instead of "c:/Code/bgr")
144
+ if (parts.length > 2) return parts.slice(-2).join('/');
145
+ return normalized;
146
+ }
147
+
127
148
  // ─── JSX Components ───
128
149
 
129
150
  function ProcessRow({ p, animate }: { p: ProcessData; animate?: boolean }) {
@@ -170,6 +191,9 @@ function ProcessRow({ p, animate }: { p: ProcessData; animate?: boolean }) {
170
191
  <button className="action-btn warning" data-action="restart" data-name={p.name} title="Restart">
171
192
  <RestartIcon />
172
193
  </button>
194
+ <button className="action-btn deploy" data-action="deploy" data-name={p.name} title="Deploy (git pull + restart)">
195
+ <DeployIcon />
196
+ </button>
173
197
  <button className="action-btn danger" data-action="delete" data-name={p.name} title="Delete">
174
198
  <TrashIcon />
175
199
  </button>
@@ -179,13 +203,14 @@ function ProcessRow({ p, animate }: { p: ProcessData; animate?: boolean }) {
179
203
  }
180
204
 
181
205
  function GroupHeader({ name, running, total, collapsed }: { name: string; running: number; total: number; collapsed: boolean }) {
206
+ // Show short folder name as label, full path as title
207
+ const shortName = shortenPath(name);
182
208
  return (
183
209
  <tr className={`group-header ${collapsed ? 'collapsed' : ''}`} data-group-name={name}>
184
210
  <td colSpan={8}>
185
- <div className="group-label">
211
+ <div className="group-label" title={name}>
186
212
  <svg className="group-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M9 18l6-6-6-6" /></svg>
187
- <span className="group-icon">📂</span>
188
- <span className="group-name">{name}</span>
213
+ <span className="group-name">{shortName}</span>
189
214
  <span className="group-counts">
190
215
  <span className={`group-count-running ${running > 0 ? 'has-running' : ''}`}>{running} running</span>
191
216
  <span className="group-count-sep">·</span>
@@ -245,6 +270,9 @@ function ProcessCard({ p }: { p: ProcessData }) {
245
270
  <button className="action-btn warning" data-action="restart" data-name={p.name} title="Restart">
246
271
  <RestartIcon /> Restart
247
272
  </button>
273
+ <button className="action-btn deploy" data-action="deploy" data-name={p.name} title="Deploy (git pull + restart)">
274
+ <DeployIcon /> Deploy
275
+ </button>
248
276
  <button className="action-btn danger" data-action="delete" data-name={p.name} title="Delete">
249
277
  <TrashIcon /> Delete
250
278
  </button>
@@ -338,6 +366,7 @@ export default function mount(): () => void {
338
366
  let drawerProcess: string | null = null;
339
367
  let drawerTab: 'stdout' | 'stderr' = 'stdout';
340
368
  let activeSection = 'logs'; // Which accordion section is open: 'info' | 'config' | 'logs'
369
+ let mutationUntil = 0; // Timestamp: ignore SSE updates until this time (after mutations)
341
370
  let configSubtab = 'toml'; // 'toml' | 'env'
342
371
  let logAutoScroll = localStorage.getItem('bgr_autoscroll') === 'true'; // OFF by default
343
372
  let logSearch = '';
@@ -425,7 +454,7 @@ export default function mount(): () => void {
425
454
 
426
455
  const animate = isFirstLoad;
427
456
 
428
- // Always group by working directory
457
+ // Group by working directory
429
458
  const groups: Record<string, ProcessData[]> = {};
430
459
  processes.forEach(p => {
431
460
  const key = p.directory || 'Unknown';
@@ -436,24 +465,18 @@ export default function mount(): () => void {
436
465
  const nodes: Node[] = [];
437
466
  const sortedGroupKeys = Object.keys(groups).sort();
438
467
 
439
- // If only one group, don't show group header
440
- if (sortedGroupKeys.length === 1) {
441
- groups[sortedGroupKeys[0]].forEach(p => {
442
- nodes.push(<ProcessRow p={p} animate={animate} /> as unknown as Node);
443
- });
444
- } else {
445
- sortedGroupKeys.forEach(groupDir => {
446
- const procs = groups[groupDir];
447
- const running = procs.filter(p => p.running).length;
448
- const collapsed = collapsedGroups.has(groupDir);
449
- nodes.push(<GroupHeader name={groupDir} running={running} total={procs.length} collapsed={collapsed} /> as unknown as Node);
450
- if (!collapsed) {
451
- procs.forEach(p => {
452
- nodes.push(<ProcessRow p={p} animate={animate} /> as unknown as Node);
453
- });
454
- }
455
- });
456
- }
468
+ // Always show group headers for every directory
469
+ sortedGroupKeys.forEach(groupDir => {
470
+ const procs = groups[groupDir];
471
+ const running = procs.filter(p => p.running).length;
472
+ const collapsed = collapsedGroups.has(groupDir);
473
+ nodes.push(<GroupHeader name={groupDir} running={running} total={procs.length} collapsed={collapsed} /> as unknown as Node);
474
+ if (!collapsed) {
475
+ procs.forEach(p => {
476
+ nodes.push(<ProcessRow p={p} animate={animate} /> as unknown as Node);
477
+ });
478
+ }
479
+ });
457
480
 
458
481
  tbody.replaceChildren(...nodes);
459
482
 
@@ -539,6 +562,7 @@ export default function mount(): () => void {
539
562
  showToast(`Failed to stop "${name}"`, 'error');
540
563
  }
541
564
  await loadProcessesFresh();
565
+ mutationUntil = Date.now() + 3000;
542
566
  break;
543
567
  }
544
568
 
@@ -562,6 +586,7 @@ export default function mount(): () => void {
562
586
  showToast(`Failed to restart "${name}"`, 'error');
563
587
  }
564
588
  await loadProcessesFresh();
589
+ mutationUntil = Date.now() + 3000;
565
590
  break;
566
591
  }
567
592
 
@@ -583,6 +608,25 @@ export default function mount(): () => void {
583
608
  showToast(`Failed to delete "${name}"`, 'error');
584
609
  }
585
610
  await loadProcessesFresh();
611
+ mutationUntil = Date.now() + 3000;
612
+ break;
613
+ }
614
+
615
+ case 'deploy': {
616
+ showToast(`Deploying "${name}"...`, 'info');
617
+ try {
618
+ const res = await fetch(`/api/deploy/${encodeURIComponent(name)}`, { method: 'POST' });
619
+ const data = await res.json();
620
+ if (res.ok) {
621
+ showToast(`Deployed "${name}" successfully`, 'success');
622
+ } else {
623
+ showToast(data.error || `Failed to deploy "${name}"`, 'error');
624
+ }
625
+ } catch {
626
+ showToast(`Failed to deploy "${name}"`, 'error');
627
+ }
628
+ await loadProcessesFresh();
629
+ mutationUntil = Date.now() + 5000;
586
630
  break;
587
631
  }
588
632
 
@@ -1173,6 +1217,8 @@ export default function mount(): () => void {
1173
1217
  function connectSSE() {
1174
1218
  eventSource = new EventSource('/api/events');
1175
1219
  eventSource.onmessage = (event) => {
1220
+ // Skip SSE updates briefly after mutations to avoid flicker
1221
+ if (Date.now() < mutationUntil) return;
1176
1222
  try {
1177
1223
  allProcesses = JSON.parse(event.data);
1178
1224
  // Throttle table re-renders to avoid lag on rapid SSE
@@ -76,7 +76,7 @@ export default function DashboardPage() {
76
76
  <th style={{ width: '70px' }}>Port</th>
77
77
  <th style={{ width: '70px' }}>Memory</th>
78
78
  <th>Command</th>
79
- <th style={{ width: '80px' }}>Runtime</th>
79
+ <th style={{ width: '100px' }}>Runtime</th>
80
80
  <th style={{ width: '150px' }}>Actions</th>
81
81
  </tr>
82
82
  </thead>
package/dist/index.js CHANGED
@@ -1,16 +1,23 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ var __defProp = Object.defineProperty;
4
+ var __export = (target, all) => {
5
+ for (var name in all)
6
+ __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true,
9
+ configurable: true,
10
+ set: (newValue) => all[name] = () => newValue
11
+ });
12
+ };
13
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
3
14
  var __require = import.meta.require;
4
15
 
5
- // src/index.ts
6
- import { parseArgs } from "util";
7
-
8
16
  // src/platform.ts
9
17
  import * as fs from "fs";
10
18
  import * as os from "os";
11
19
  var {$ } = globalThis.Bun;
12
20
  import { createMeasure } from "measure-fn";
13
- var plat = createMeasure("platform");
14
21
  function isWindows() {
15
22
  return process.platform === "win32";
16
23
  }
@@ -312,8 +319,150 @@ async function getProcessPorts(pid) {
312
319
  return [];
313
320
  }
314
321
  }
322
+ var plat;
323
+ var init_platform = __esm(() => {
324
+ plat = createMeasure("platform");
325
+ });
326
+
327
+ // src/db.ts
328
+ var exports_db = {};
329
+ __export(exports_db, {
330
+ updateProcessPid: () => updateProcessPid,
331
+ retryDatabaseOperation: () => retryDatabaseOperation,
332
+ removeProcessByName: () => removeProcessByName,
333
+ removeProcess: () => removeProcess,
334
+ removeAllProcesses: () => removeAllProcesses,
335
+ insertProcess: () => insertProcess,
336
+ getProcess: () => getProcess,
337
+ getDbInfo: () => getDbInfo,
338
+ getAllProcesses: () => getAllProcesses,
339
+ dbPath: () => dbPath,
340
+ db: () => db,
341
+ bgrHome: () => bgrHome,
342
+ ProcessSchema: () => ProcessSchema
343
+ });
344
+ import { Database, z } from "sqlite-zod-orm";
345
+ import { join } from "path";
346
+ var {sleep } = globalThis.Bun;
347
+ import { existsSync as existsSync3, copyFileSync as copyFileSync2 } from "fs";
348
+ function getProcess(name) {
349
+ return db.process.select().where({ name }).orderBy("timestamp", "desc").limit(1).get() || null;
350
+ }
351
+ function getAllProcesses() {
352
+ return db.process.select().all();
353
+ }
354
+ function insertProcess(data) {
355
+ return db.process.insert({
356
+ ...data,
357
+ timestamp: new Date().toISOString()
358
+ });
359
+ }
360
+ function removeProcess(pid) {
361
+ const matches = db.process.select().where({ pid }).all();
362
+ for (const p of matches) {
363
+ db.process.delete(p.id);
364
+ }
365
+ }
366
+ function removeProcessByName(name) {
367
+ const matches = db.process.select().where({ name }).all();
368
+ for (const p of matches) {
369
+ db.process.delete(p.id);
370
+ }
371
+ }
372
+ function updateProcessPid(name, newPid) {
373
+ const proc = db.process.select().where({ name }).limit(1).get();
374
+ if (proc) {
375
+ db.process.update(proc.id, { pid: newPid });
376
+ }
377
+ }
378
+ function removeAllProcesses() {
379
+ const all = db.process.select().all();
380
+ for (const p of all) {
381
+ db.process.delete(p.id);
382
+ }
383
+ }
384
+ function getDbInfo() {
385
+ return {
386
+ dbPath,
387
+ bgrHome,
388
+ dbFilename,
389
+ exists: existsSync3(dbPath)
390
+ };
391
+ }
392
+ async function retryDatabaseOperation(operation, maxRetries = 5, delay = 100) {
393
+ for (let attempt = 1;attempt <= maxRetries; attempt++) {
394
+ try {
395
+ return operation();
396
+ } catch (err) {
397
+ if (err?.code === "SQLITE_BUSY" && attempt < maxRetries) {
398
+ await sleep(delay * attempt);
399
+ continue;
400
+ }
401
+ throw err;
402
+ }
403
+ }
404
+ throw new Error("Max retries reached for database operation");
405
+ }
406
+ var ProcessSchema, homePath, bgrDir, dbFilename, dbPath, bgrHome, legacyDbPath, db;
407
+ var init_db = __esm(() => {
408
+ init_platform();
409
+ ProcessSchema = z.object({
410
+ pid: z.number(),
411
+ workdir: z.string(),
412
+ command: z.string(),
413
+ name: z.string(),
414
+ env: z.string(),
415
+ configPath: z.string().default(""),
416
+ stdout_path: z.string(),
417
+ stderr_path: z.string(),
418
+ timestamp: z.string().default(() => new Date().toISOString())
419
+ });
420
+ homePath = getHomeDir();
421
+ bgrDir = join(homePath, ".bgr");
422
+ ensureDir(bgrDir);
423
+ dbFilename = process.env.BGRUN_DB ?? "bgrun.sqlite";
424
+ dbPath = join(bgrDir, dbFilename);
425
+ bgrHome = bgrDir;
426
+ legacyDbPath = join(bgrDir, "bgr_v2.sqlite");
427
+ if (!existsSync3(dbPath) && existsSync3(legacyDbPath)) {
428
+ try {
429
+ copyFileSync2(legacyDbPath, dbPath);
430
+ console.log(`[bgrun] Migrated database: ${legacyDbPath} \u2192 ${dbPath}`);
431
+ } catch (e) {}
432
+ }
433
+ db = new Database(dbPath, {
434
+ process: ProcessSchema
435
+ }, {
436
+ indexes: {
437
+ process: ["name", "timestamp", "pid"]
438
+ }
439
+ });
440
+ });
441
+
442
+ // src/server.ts
443
+ var exports_server = {};
444
+ __export(exports_server, {
445
+ startServer: () => startServer
446
+ });
447
+ import path2 from "path";
448
+ async function startServer() {
449
+ const { start } = await import("melina");
450
+ const appDir = path2.join(import.meta.dir, "../dashboard/app");
451
+ const explicitPort = process.env.BUN_PORT ? parseInt(process.env.BUN_PORT, 10) : undefined;
452
+ await start({
453
+ appDir,
454
+ defaultTitle: "bgrun Dashboard - Process Manager",
455
+ globalCss: path2.join(appDir, "globals.css"),
456
+ ...explicitPort !== undefined && { port: explicitPort }
457
+ });
458
+ }
459
+ var init_server = () => {};
460
+
461
+ // src/index.ts
462
+ import { parseArgs } from "util";
315
463
 
316
464
  // src/utils.ts
465
+ init_platform();
317
466
  import * as fs2 from "fs";
318
467
  import chalk from "chalk";
319
468
  function parseEnvString(envString) {
@@ -391,94 +540,9 @@ function tailFile(path, prefix, colorFn, lines) {
391
540
  };
392
541
  }
393
542
 
394
- // src/db.ts
395
- import { Database, z } from "sqlite-zod-orm";
396
- import { join } from "path";
397
- var {sleep } = globalThis.Bun;
398
- import { existsSync as existsSync3, copyFileSync as copyFileSync2 } from "fs";
399
- var ProcessSchema = z.object({
400
- pid: z.number(),
401
- workdir: z.string(),
402
- command: z.string(),
403
- name: z.string(),
404
- env: z.string(),
405
- configPath: z.string().default(""),
406
- stdout_path: z.string(),
407
- stderr_path: z.string(),
408
- timestamp: z.string().default(() => new Date().toISOString())
409
- });
410
- var homePath = getHomeDir();
411
- var bgrDir = join(homePath, ".bgr");
412
- ensureDir(bgrDir);
413
- var dbFilename = process.env.BGRUN_DB ?? "bgrun.sqlite";
414
- var dbPath = join(bgrDir, dbFilename);
415
- var bgrHome = bgrDir;
416
- var legacyDbPath = join(bgrDir, "bgr_v2.sqlite");
417
- if (!existsSync3(dbPath) && existsSync3(legacyDbPath)) {
418
- try {
419
- copyFileSync2(legacyDbPath, dbPath);
420
- console.log(`[bgrun] Migrated database: ${legacyDbPath} \u2192 ${dbPath}`);
421
- } catch (e) {}
422
- }
423
- var db = new Database(dbPath, {
424
- process: ProcessSchema
425
- }, {
426
- indexes: {
427
- process: ["name", "timestamp", "pid"]
428
- }
429
- });
430
- function getProcess(name) {
431
- return db.process.select().where({ name }).orderBy("timestamp", "desc").limit(1).get() || null;
432
- }
433
- function getAllProcesses() {
434
- return db.process.select().all();
435
- }
436
- function insertProcess(data) {
437
- return db.process.insert({
438
- ...data,
439
- timestamp: new Date().toISOString()
440
- });
441
- }
442
- function removeProcess(pid) {
443
- const matches = db.process.select().where({ pid }).all();
444
- for (const p of matches) {
445
- db.process.delete(p.id);
446
- }
447
- }
448
- function removeProcessByName(name) {
449
- const matches = db.process.select().where({ name }).all();
450
- for (const p of matches) {
451
- db.process.delete(p.id);
452
- }
453
- }
454
- function removeAllProcesses() {
455
- const all = db.process.select().all();
456
- for (const p of all) {
457
- db.process.delete(p.id);
458
- }
459
- }
460
- function getDbInfo() {
461
- return {
462
- dbPath,
463
- bgrHome,
464
- dbFilename,
465
- exists: existsSync3(dbPath)
466
- };
467
- }
468
- async function retryDatabaseOperation(operation, maxRetries = 5, delay = 100) {
469
- for (let attempt = 1;attempt <= maxRetries; attempt++) {
470
- try {
471
- return operation();
472
- } catch (err) {
473
- if (err?.code === "SQLITE_BUSY" && attempt < maxRetries) {
474
- await sleep(delay * attempt);
475
- continue;
476
- }
477
- throw err;
478
- }
479
- }
480
- throw new Error("Max retries reached for database operation");
481
- }
543
+ // src/commands/run.ts
544
+ init_db();
545
+ init_platform();
482
546
 
483
547
  // src/logger.ts
484
548
  import boxen from "boxen";
@@ -831,6 +895,8 @@ function renderProcessTable(processes, options) {
831
895
  }
832
896
 
833
897
  // src/commands/list.ts
898
+ init_db();
899
+ init_platform();
834
900
  function formatMemory(bytes) {
835
901
  if (bytes === 0)
836
902
  return "-";
@@ -904,6 +970,8 @@ async function showAll(opts) {
904
970
  }
905
971
 
906
972
  // src/commands/cleanup.ts
973
+ init_db();
974
+ init_platform();
907
975
  import * as fs3 from "fs";
908
976
  async function handleDelete(name) {
909
977
  const process2 = getProcess(name);
@@ -1020,6 +1088,8 @@ async function handleDeleteAll() {
1020
1088
  }
1021
1089
 
1022
1090
  // src/commands/watch.ts
1091
+ init_db();
1092
+ init_platform();
1023
1093
  import * as fs4 from "fs";
1024
1094
  import path from "path";
1025
1095
  import chalk5 from "chalk";
@@ -1208,6 +1278,8 @@ SIGINT received...`));
1208
1278
  }
1209
1279
 
1210
1280
  // src/commands/logs.ts
1281
+ init_db();
1282
+ init_platform();
1211
1283
  import chalk6 from "chalk";
1212
1284
  import * as fs5 from "fs";
1213
1285
  async function showLogs(name, logType = "both", lines) {
@@ -1251,6 +1323,8 @@ async function showLogs(name, logType = "both", lines) {
1251
1323
  }
1252
1324
 
1253
1325
  // src/commands/details.ts
1326
+ init_db();
1327
+ init_platform();
1254
1328
  import chalk7 from "chalk";
1255
1329
  async function showDetails(name) {
1256
1330
  const proc = getProcess(name);
@@ -1285,21 +1359,9 @@ ${Object.entries(envVars).map(([key, value]) => `${chalk7.cyan.bold(key)} = ${ch
1285
1359
  announce(details, `Process Details: ${name}`);
1286
1360
  }
1287
1361
 
1288
- // src/server.ts
1289
- import { start } from "melina";
1290
- import path2 from "path";
1291
- async function startServer() {
1292
- const appDir = path2.join(import.meta.dir, "../dashboard/app");
1293
- const explicitPort = process.env.BUN_PORT ? parseInt(process.env.BUN_PORT, 10) : undefined;
1294
- await start({
1295
- appDir,
1296
- defaultTitle: "bgrun Dashboard - Process Manager",
1297
- globalCss: path2.join(appDir, "globals.css"),
1298
- ...explicitPort !== undefined && { port: explicitPort }
1299
- });
1300
- }
1301
-
1302
1362
  // src/index.ts
1363
+ init_platform();
1364
+ init_db();
1303
1365
  import dedent from "dedent";
1304
1366
  import chalk8 from "chalk";
1305
1367
  import { join as join3 } from "path";
@@ -1323,7 +1385,9 @@ async function showHelp() {
1323
1385
  bgrun [name] Show details for a process
1324
1386
  bgrun --dashboard Launch web dashboard (managed by bgrun)
1325
1387
  bgrun --restart [name] Restart a process
1388
+ bgrun --restart-all Restart ALL registered processes
1326
1389
  bgrun --stop [name] Stop a process (keep in registry)
1390
+ bgrun --stop-all Stop ALL running processes
1327
1391
  bgrun --delete [name] Delete a process
1328
1392
  bgrun --clean Remove all stopped processes
1329
1393
  bgrun --nuke Delete ALL processes
@@ -1369,7 +1433,9 @@ async function run2() {
1369
1433
  delete: { type: "boolean" },
1370
1434
  nuke: { type: "boolean" },
1371
1435
  restart: { type: "boolean" },
1436
+ "restart-all": { type: "boolean" },
1372
1437
  stop: { type: "boolean" },
1438
+ "stop-all": { type: "boolean" },
1373
1439
  clean: { type: "boolean" },
1374
1440
  json: { type: "boolean" },
1375
1441
  logs: { type: "boolean" },
@@ -1391,7 +1457,8 @@ async function run2() {
1391
1457
  allowPositionals: true
1392
1458
  });
1393
1459
  if (values["_serve"]) {
1394
- await startServer();
1460
+ const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
1461
+ await startServer2();
1395
1462
  return;
1396
1463
  }
1397
1464
  if (values.dashboard) {
@@ -1401,14 +1468,20 @@ async function run2() {
1401
1468
  const requestedPort = values.port;
1402
1469
  const existing = getProcess(dashboardName);
1403
1470
  if (existing && await isProcessRunning(existing.pid)) {
1404
- const existingPorts = await getProcessPorts(existing.pid);
1471
+ let existingPorts = await getProcessPorts(existing.pid);
1472
+ if (existingPorts.length === 0) {
1473
+ const childPid = await findChildPid(existing.pid);
1474
+ if (childPid !== existing.pid) {
1475
+ existingPorts = await getProcessPorts(childPid);
1476
+ }
1477
+ }
1405
1478
  const portStr = existingPorts.length > 0 ? `:${existingPorts[0]}` : "(detecting...)";
1406
1479
  announce(`Dashboard is already running (PID ${existing.pid})
1407
1480
 
1408
1481
  ` + ` \uD83C\uDF10 ${chalk8.cyan(`http://localhost${portStr}`)}
1409
1482
 
1410
- ` + ` Use ${chalk8.yellow(`bgrun --stop ${dashboardName}`)} to stop it
1411
- ` + ` Use ${chalk8.yellow(`bgrun --dashboard --force`)} to restart`, "BGR Dashboard");
1483
+ Use ${chalk8.yellow(`bgrun --stop ${dashboardName}`)} to stop it
1484
+ Use ${chalk8.yellow(`bgrun --dashboard --force`)} to restart`, "BGR Dashboard");
1412
1485
  return;
1413
1486
  }
1414
1487
  if (existing) {
@@ -1514,6 +1587,61 @@ async function run2() {
1514
1587
  await handleClean();
1515
1588
  return;
1516
1589
  }
1590
+ if (values["restart-all"]) {
1591
+ const { getAllProcesses: getAllProcesses2 } = await Promise.resolve().then(() => (init_db(), exports_db));
1592
+ const all = getAllProcesses2();
1593
+ if (all.length === 0) {
1594
+ error("No processes registered.");
1595
+ return;
1596
+ }
1597
+ console.log(chalk8.bold(`
1598
+ Restarting ${all.length} processes...
1599
+ `));
1600
+ for (const proc of all) {
1601
+ try {
1602
+ console.log(chalk8.yellow(` \u21BB Restarting ${proc.name}...`));
1603
+ await handleRun({
1604
+ action: "run",
1605
+ name: proc.name,
1606
+ force: true,
1607
+ remoteName: ""
1608
+ });
1609
+ } catch (err) {
1610
+ console.error(chalk8.red(` \u2717 Failed to restart ${proc.name}: ${err.message}`));
1611
+ }
1612
+ }
1613
+ console.log(chalk8.green(`
1614
+ \u2713 All processes restarted.
1615
+ `));
1616
+ return;
1617
+ }
1618
+ if (values["stop-all"]) {
1619
+ const { getAllProcesses: getAllProcesses2 } = await Promise.resolve().then(() => (init_db(), exports_db));
1620
+ const all = getAllProcesses2();
1621
+ if (all.length === 0) {
1622
+ error("No processes registered.");
1623
+ return;
1624
+ }
1625
+ console.log(chalk8.bold(`
1626
+ Stopping ${all.length} processes...
1627
+ `));
1628
+ for (const proc of all) {
1629
+ try {
1630
+ if (await isProcessRunning(proc.pid)) {
1631
+ console.log(chalk8.yellow(` \u25A0 Stopping ${proc.name} (PID ${proc.pid})...`));
1632
+ await handleStop(proc.name);
1633
+ } else {
1634
+ console.log(chalk8.gray(` \u25CB ${proc.name} already stopped`));
1635
+ }
1636
+ } catch (err) {
1637
+ console.error(chalk8.red(` \u2717 Failed to stop ${proc.name}: ${err.message}`));
1638
+ }
1639
+ }
1640
+ console.log(chalk8.green(`
1641
+ \u2713 All processes stopped.
1642
+ `));
1643
+ return;
1644
+ }
1517
1645
  const name = values.name || positionals[0];
1518
1646
  if (values.delete) {
1519
1647
  if (name) {
package/image.png ADDED
Binary file
package/package.json CHANGED
@@ -1,59 +1,60 @@
1
- {
2
- "name": "bgrun",
3
- "version": "3.7.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
- "examples/bgr-startup.sh"
24
- ],
25
- "keywords": [
26
- "process-manager",
27
- "bun",
28
- "monitoring",
29
- "devops",
30
- "deployment",
31
- "background",
32
- "daemon"
33
- ],
34
- "author": "7flash",
35
- "license": "MIT",
36
- "repository": {
37
- "type": "git",
38
- "url": "https://github.com/Mements/bgr.git"
39
- },
40
- "devDependencies": {
41
- "bun-types": "latest"
42
- },
43
- "peerDependencies": {
44
- "typescript": "^5.0.0"
45
- },
46
- "dependencies": {
47
- "boxen": "^8.0.1",
48
- "chalk": "^5.4.1",
49
- "dedent": "^1.5.3",
50
- "measure-fn": "^3.2.1",
51
- "melina": "^2.2.1",
52
- "react": "^19.2.4",
53
- "react-dom": "^19.2.4",
54
- "sqlite-zod-orm": "^3.8.0"
55
- },
56
- "engines": {
57
- "bun": ">=1.0.0"
58
- }
1
+ {
2
+ "name": "bgrun",
3
+ "version": "3.8.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
+ "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
+ }
59
60
  }
package/src/api.ts CHANGED
@@ -48,3 +48,16 @@ export { handleRun } from './commands/run'
48
48
 
49
49
  // --- Utilities ---
50
50
  export { getVersion, calculateRuntime, parseEnvString, validateDirectory } from './utils'
51
+
52
+ // --- Default Export (namespace style) ---
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'
55
+ import { handleRun } from './commands/run'
56
+ import { getVersion, calculateRuntime, parseEnvString, validateDirectory } from './utils'
57
+
58
+ export default {
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,
61
+ handleRun,
62
+ getVersion, calculateRuntime, parseEnvString, validateDirectory,
63
+ }
package/src/index.ts CHANGED
@@ -10,7 +10,8 @@ import { showLogs } from "./commands/logs";
10
10
  import { showDetails } from "./commands/details";
11
11
  import type { CommandOptions } from "./types";
12
12
  import { error, announce } from "./logger";
13
- import { startServer } from "./server";
13
+ // startServer is dynamically imported only when --_serve is used
14
+ // to avoid loading melina (which has side-effects) on every bgrun command
14
15
  import { getHomeDir, getShellCommand, findChildPid, isProcessRunning, terminateProcess, getProcessPorts, killProcessOnPort, waitForPortFree } from "./platform";
15
16
  import { insertProcess, removeProcessByName, getProcess, retryDatabaseOperation, getDbInfo } from "./db";
16
17
  import dedent from "dedent";
@@ -38,7 +39,9 @@ async function showHelp() {
38
39
  bgrun [name] Show details for a process
39
40
  bgrun --dashboard Launch web dashboard (managed by bgrun)
40
41
  bgrun --restart [name] Restart a process
42
+ bgrun --restart-all Restart ALL registered processes
41
43
  bgrun --stop [name] Stop a process (keep in registry)
44
+ bgrun --stop-all Stop ALL running processes
42
45
  bgrun --delete [name] Delete a process
43
46
  bgrun --clean Remove all stopped processes
44
47
  bgrun --nuke Delete ALL processes
@@ -86,7 +89,9 @@ async function run() {
86
89
  delete: { type: 'boolean' },
87
90
  nuke: { type: 'boolean' },
88
91
  restart: { type: 'boolean' },
92
+ "restart-all": { type: 'boolean' },
89
93
  stop: { type: 'boolean' },
94
+ "stop-all": { type: 'boolean' },
90
95
  clean: { type: 'boolean' },
91
96
  json: { type: 'boolean' },
92
97
  logs: { type: 'boolean' },
@@ -112,6 +117,7 @@ async function run() {
112
117
  // Port is NOT passed explicitly — Melina auto-detects from BUN_PORT env
113
118
  // or defaults to 3000 with fallback to next available port.
114
119
  if (values['_serve']) {
120
+ const { startServer } = await import("./server");
115
121
  await startServer();
116
122
  return;
117
123
  }
@@ -128,7 +134,15 @@ async function run() {
128
134
  // Check if dashboard is already running
129
135
  const existing = getProcess(dashboardName);
130
136
  if (existing && await isProcessRunning(existing.pid)) {
131
- const existingPorts = await getProcessPorts(existing.pid);
137
+ // The stored PID may be the shell wrapper (cmd.exe), not the actual bun process
138
+ // Try the stored PID first, then traverse the process tree to find the real one
139
+ let existingPorts = await getProcessPorts(existing.pid);
140
+ if (existingPorts.length === 0) {
141
+ const childPid = await findChildPid(existing.pid);
142
+ if (childPid !== existing.pid) {
143
+ existingPorts = await getProcessPorts(childPid);
144
+ }
145
+ }
132
146
  const portStr = existingPorts.length > 0 ? `:${existingPorts[0]}` : '(detecting...)';
133
147
  announce(
134
148
  `Dashboard is already running (PID ${existing.pid})\n\n` +
@@ -270,6 +284,57 @@ async function run() {
270
284
  return;
271
285
  }
272
286
 
287
+ // Restart all registered processes
288
+ if (values['restart-all']) {
289
+ const { getAllProcesses } = await import('./db');
290
+ const all = getAllProcesses();
291
+ if (all.length === 0) {
292
+ error('No processes registered.');
293
+ return;
294
+ }
295
+ console.log(chalk.bold(`\n Restarting ${all.length} processes...\n`));
296
+ for (const proc of all) {
297
+ try {
298
+ console.log(chalk.yellow(` ↻ Restarting ${proc.name}...`));
299
+ await handleRun({
300
+ action: 'run',
301
+ name: proc.name,
302
+ force: true,
303
+ remoteName: '',
304
+ });
305
+ } catch (err: any) {
306
+ console.error(chalk.red(` ✗ Failed to restart ${proc.name}: ${err.message}`));
307
+ }
308
+ }
309
+ console.log(chalk.green(`\n ✓ All processes restarted.\n`));
310
+ return;
311
+ }
312
+
313
+ // Stop all running processes
314
+ if (values['stop-all']) {
315
+ const { getAllProcesses } = await import('./db');
316
+ const all = getAllProcesses();
317
+ if (all.length === 0) {
318
+ error('No processes registered.');
319
+ return;
320
+ }
321
+ console.log(chalk.bold(`\n Stopping ${all.length} processes...\n`));
322
+ for (const proc of all) {
323
+ try {
324
+ if (await isProcessRunning(proc.pid)) {
325
+ console.log(chalk.yellow(` ■ Stopping ${proc.name} (PID ${proc.pid})...`));
326
+ await handleStop(proc.name);
327
+ } else {
328
+ console.log(chalk.gray(` ○ ${proc.name} already stopped`));
329
+ }
330
+ } catch (err: any) {
331
+ console.error(chalk.red(` ✗ Failed to stop ${proc.name}: ${err.message}`));
332
+ }
333
+ }
334
+ console.log(chalk.green(`\n ✓ All processes stopped.\n`));
335
+ return;
336
+ }
337
+
273
338
  const name = (values.name as string) || positionals[0];
274
339
 
275
340
  // Delete
package/src/server.ts CHANGED
@@ -8,10 +8,11 @@
8
8
  * - If BUN_PORT env var is set → uses that (explicit, will fail if busy)
9
9
  * - Otherwise → defaults to 3000, falls back to next available if busy
10
10
  */
11
- import { start } from 'melina';
12
11
  import path from 'path';
13
12
 
14
13
  export async function startServer() {
14
+ // Dynamic import to avoid melina's side-effect console.log at bundle load time
15
+ const { start } = await import('melina');
15
16
  const appDir = path.join(import.meta.dir, '../dashboard/app');
16
17
 
17
18
  // Only pass port when BUN_PORT is explicitly set.
package/src/types.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  export interface CommandOptions {
2
- remoteName: string;
2
+ remoteName?: string;
3
3
  command?: string;
4
4
  directory?: string;
5
5
  env?: Record<string, string>;
6
6
  configPath?: string;
7
- action: string;
7
+ action?: string;
8
8
  name?: string;
9
9
  force?: boolean;
10
10
  fetch?: boolean;