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 +26 -3
- package/dashboard/app/api/deploy/[name]/route.ts +58 -0
- package/dashboard/app/globals.css +30 -27
- package/dashboard/app/page.client.tsx +75 -29
- package/dashboard/app/page.tsx +1 -1
- package/dist/index.js +238 -110
- package/image.png +0 -0
- package/package.json +59 -58
- package/src/api.ts +13 -0
- package/src/index.ts +67 -2
- package/src/server.ts +2 -1
- package/src/types.ts +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<img src="./image.png" alt="bgrun" width="600" />
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Production-ready process manager with dashboard and programmatic API, designed for running your containers, services, and AI agents.**
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/bgrun)
|
|
8
8
|
[](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
|
|
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.
|
|
656
|
-
background:
|
|
657
|
-
border:
|
|
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.
|
|
668
|
-
font-size: 0.
|
|
669
|
-
font-weight:
|
|
670
|
-
text-
|
|
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:
|
|
677
|
-
height:
|
|
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-
|
|
696
|
-
font-size: 0.
|
|
694
|
+
color: var(--text-muted);
|
|
695
|
+
font-size: 0.6875rem;
|
|
697
696
|
font-family: var(--font-mono);
|
|
698
|
-
letter-spacing: 0.
|
|
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.
|
|
707
|
-
margin-left:
|
|
708
|
-
font-size: 0.
|
|
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(--
|
|
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:
|
|
21
|
-
|
|
22
|
-
memory: number;
|
|
23
|
-
|
|
24
|
-
|
|
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-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
440
|
-
|
|
441
|
-
groups[
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
package/dashboard/app/page.tsx
CHANGED
|
@@ -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: '
|
|
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/
|
|
395
|
-
|
|
396
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1411
|
-
|
|
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.
|
|
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
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"react
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2
|
+
remoteName?: string;
|
|
3
3
|
command?: string;
|
|
4
4
|
directory?: string;
|
|
5
5
|
env?: Record<string, string>;
|
|
6
6
|
configPath?: string;
|
|
7
|
-
action
|
|
7
|
+
action?: string;
|
|
8
8
|
name?: string;
|
|
9
9
|
force?: boolean;
|
|
10
10
|
fetch?: boolean;
|