bgrun 3.7.0 → 3.7.1
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/globals.css +24 -27
- package/dashboard/app/page.client.tsx +40 -29
- package/dashboard/app/page.tsx +1 -1
- package/dist/index.js +238 -110
- package/image.png +0 -0
- package/package.json +60 -59
- 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>
|
|
@@ -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) ─── */
|
|
@@ -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;
|
|
@@ -124,6 +123,17 @@ function formatTimeAgo(date: Date): string {
|
|
|
124
123
|
return `${days}d ago`;
|
|
125
124
|
}
|
|
126
125
|
|
|
126
|
+
// ─── Helpers ───
|
|
127
|
+
|
|
128
|
+
function shortenPath(dir: string): string {
|
|
129
|
+
if (!dir) return '';
|
|
130
|
+
const normalized = dir.replace(/\\/g, '/');
|
|
131
|
+
const parts = normalized.split('/');
|
|
132
|
+
// Show last 2 segments (e.g. "Code/bgr" instead of "c:/Code/bgr")
|
|
133
|
+
if (parts.length > 2) return parts.slice(-2).join('/');
|
|
134
|
+
return normalized;
|
|
135
|
+
}
|
|
136
|
+
|
|
127
137
|
// ─── JSX Components ───
|
|
128
138
|
|
|
129
139
|
function ProcessRow({ p, animate }: { p: ProcessData; animate?: boolean }) {
|
|
@@ -179,13 +189,14 @@ function ProcessRow({ p, animate }: { p: ProcessData; animate?: boolean }) {
|
|
|
179
189
|
}
|
|
180
190
|
|
|
181
191
|
function GroupHeader({ name, running, total, collapsed }: { name: string; running: number; total: number; collapsed: boolean }) {
|
|
192
|
+
// Show short folder name as label, full path as title
|
|
193
|
+
const shortName = shortenPath(name);
|
|
182
194
|
return (
|
|
183
195
|
<tr className={`group-header ${collapsed ? 'collapsed' : ''}`} data-group-name={name}>
|
|
184
196
|
<td colSpan={8}>
|
|
185
|
-
<div className="group-label">
|
|
197
|
+
<div className="group-label" title={name}>
|
|
186
198
|
<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>
|
|
199
|
+
<span className="group-name">{shortName}</span>
|
|
189
200
|
<span className="group-counts">
|
|
190
201
|
<span className={`group-count-running ${running > 0 ? 'has-running' : ''}`}>{running} running</span>
|
|
191
202
|
<span className="group-count-sep">·</span>
|
|
@@ -338,6 +349,7 @@ export default function mount(): () => void {
|
|
|
338
349
|
let drawerProcess: string | null = null;
|
|
339
350
|
let drawerTab: 'stdout' | 'stderr' = 'stdout';
|
|
340
351
|
let activeSection = 'logs'; // Which accordion section is open: 'info' | 'config' | 'logs'
|
|
352
|
+
let mutationUntil = 0; // Timestamp: ignore SSE updates until this time (after mutations)
|
|
341
353
|
let configSubtab = 'toml'; // 'toml' | 'env'
|
|
342
354
|
let logAutoScroll = localStorage.getItem('bgr_autoscroll') === 'true'; // OFF by default
|
|
343
355
|
let logSearch = '';
|
|
@@ -425,7 +437,7 @@ export default function mount(): () => void {
|
|
|
425
437
|
|
|
426
438
|
const animate = isFirstLoad;
|
|
427
439
|
|
|
428
|
-
//
|
|
440
|
+
// Group by working directory
|
|
429
441
|
const groups: Record<string, ProcessData[]> = {};
|
|
430
442
|
processes.forEach(p => {
|
|
431
443
|
const key = p.directory || 'Unknown';
|
|
@@ -436,24 +448,18 @@ export default function mount(): () => void {
|
|
|
436
448
|
const nodes: Node[] = [];
|
|
437
449
|
const sortedGroupKeys = Object.keys(groups).sort();
|
|
438
450
|
|
|
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
|
-
}
|
|
451
|
+
// Always show group headers for every directory
|
|
452
|
+
sortedGroupKeys.forEach(groupDir => {
|
|
453
|
+
const procs = groups[groupDir];
|
|
454
|
+
const running = procs.filter(p => p.running).length;
|
|
455
|
+
const collapsed = collapsedGroups.has(groupDir);
|
|
456
|
+
nodes.push(<GroupHeader name={groupDir} running={running} total={procs.length} collapsed={collapsed} /> as unknown as Node);
|
|
457
|
+
if (!collapsed) {
|
|
458
|
+
procs.forEach(p => {
|
|
459
|
+
nodes.push(<ProcessRow p={p} animate={animate} /> as unknown as Node);
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
});
|
|
457
463
|
|
|
458
464
|
tbody.replaceChildren(...nodes);
|
|
459
465
|
|
|
@@ -539,6 +545,7 @@ export default function mount(): () => void {
|
|
|
539
545
|
showToast(`Failed to stop "${name}"`, 'error');
|
|
540
546
|
}
|
|
541
547
|
await loadProcessesFresh();
|
|
548
|
+
mutationUntil = Date.now() + 3000;
|
|
542
549
|
break;
|
|
543
550
|
}
|
|
544
551
|
|
|
@@ -562,6 +569,7 @@ export default function mount(): () => void {
|
|
|
562
569
|
showToast(`Failed to restart "${name}"`, 'error');
|
|
563
570
|
}
|
|
564
571
|
await loadProcessesFresh();
|
|
572
|
+
mutationUntil = Date.now() + 3000;
|
|
565
573
|
break;
|
|
566
574
|
}
|
|
567
575
|
|
|
@@ -583,6 +591,7 @@ export default function mount(): () => void {
|
|
|
583
591
|
showToast(`Failed to delete "${name}"`, 'error');
|
|
584
592
|
}
|
|
585
593
|
await loadProcessesFresh();
|
|
594
|
+
mutationUntil = Date.now() + 3000;
|
|
586
595
|
break;
|
|
587
596
|
}
|
|
588
597
|
|
|
@@ -1173,6 +1182,8 @@ export default function mount(): () => void {
|
|
|
1173
1182
|
function connectSSE() {
|
|
1174
1183
|
eventSource = new EventSource('/api/events');
|
|
1175
1184
|
eventSource.onmessage = (event) => {
|
|
1185
|
+
// Skip SSE updates briefly after mutations to avoid flicker
|
|
1186
|
+
if (Date.now() < mutationUntil) return;
|
|
1176
1187
|
try {
|
|
1177
1188
|
allProcesses = JSON.parse(event.data);
|
|
1178
1189
|
// 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.7.
|
|
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
|
-
|
|
59
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "bgrun",
|
|
3
|
+
"version": "3.7.1",
|
|
4
|
+
"description": "bgrun — A lightweight process manager for Bun",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/api.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/api.ts"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"bgrun": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "bun run ./src/build.ts",
|
|
15
|
+
"test": "bun test",
|
|
16
|
+
"prepublishOnly": "bun run build"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"src",
|
|
21
|
+
"dashboard/app",
|
|
22
|
+
"README.md",
|
|
23
|
+
"image.png",
|
|
24
|
+
"examples/bgr-startup.sh"
|
|
25
|
+
],
|
|
26
|
+
"keywords": [
|
|
27
|
+
"process-manager",
|
|
28
|
+
"bun",
|
|
29
|
+
"monitoring",
|
|
30
|
+
"devops",
|
|
31
|
+
"deployment",
|
|
32
|
+
"background",
|
|
33
|
+
"daemon"
|
|
34
|
+
],
|
|
35
|
+
"author": "7flash",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/7flash/bgrun.git"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"bun-types": "latest"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"typescript": "^5.0.0"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"boxen": "^8.0.1",
|
|
49
|
+
"chalk": "^5.4.1",
|
|
50
|
+
"dedent": "^1.5.3",
|
|
51
|
+
"measure-fn": "^3.2.1",
|
|
52
|
+
"melina": "^2.2.1",
|
|
53
|
+
"react": "^19.2.4",
|
|
54
|
+
"react-dom": "^19.2.4",
|
|
55
|
+
"sqlite-zod-orm": "^3.8.0"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"bun": ">=1.0.0"
|
|
59
|
+
}
|
|
60
|
+
}
|
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;
|