bm2 1.0.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/LICENSE +674 -0
- package/README.md +1591 -0
- package/package.json +68 -0
- package/src/cluster-manager.ts +117 -0
- package/src/constants.ts +44 -0
- package/src/cron-manager.ts +74 -0
- package/src/daemon.ts +233 -0
- package/src/dashboard-ui.ts +285 -0
- package/src/dashboard.ts +203 -0
- package/src/deploy.ts +188 -0
- package/src/env-manager.ts +77 -0
- package/src/graceful-reload.ts +71 -0
- package/src/health-checker.ts +112 -0
- package/src/index.ts +15 -0
- package/src/log-manager.ts +201 -0
- package/src/module-manager.ts +119 -0
- package/src/monitor.ts +185 -0
- package/src/process-container.ts +508 -0
- package/src/process-manager.ts +403 -0
- package/src/startup.ts +158 -0
- package/src/types.ts +230 -0
- package/src/utils.ts +171 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BM2 — Bun Process Manager
|
|
3
|
+
* A production-grade process manager for Bun.
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Fork & cluster execution modes
|
|
7
|
+
* - Auto-restart & crash recovery
|
|
8
|
+
* - Health checks & monitoring
|
|
9
|
+
* - Log management & rotation
|
|
10
|
+
* - Deployment support
|
|
11
|
+
*
|
|
12
|
+
* https://github.com/your-org/bm2
|
|
13
|
+
* License: GPL-3.0-only
|
|
14
|
+
* Author: Zak <zak@maxxpainn.com>
|
|
15
|
+
*/
|
|
16
|
+
export function getDashboardHTML(): string {
|
|
17
|
+
return `<!DOCTYPE html>
|
|
18
|
+
<html lang="en">
|
|
19
|
+
<head>
|
|
20
|
+
<meta charset="UTF-8">
|
|
21
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
22
|
+
<title>BM2 Dashboard</title>
|
|
23
|
+
<style>
|
|
24
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
25
|
+
:root {
|
|
26
|
+
--bg: #0d1117; --surface: #161b22; --border: #30363d;
|
|
27
|
+
--text: #c9d1d9; --text-dim: #8b949e; --accent: #58a6ff;
|
|
28
|
+
--green: #3fb950; --red: #f85149; --yellow: #d29922;
|
|
29
|
+
--orange: #db6d28; --purple: #bc8cff;
|
|
30
|
+
}
|
|
31
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', monospace; background: var(--bg); color: var(--text); }
|
|
32
|
+
.header { background: var(--surface); border-bottom: 1px solid var(--border); padding: 16px 24px; display: flex; align-items: center; justify-content: space-between; }
|
|
33
|
+
.header h1 { font-size: 20px; color: var(--accent); }
|
|
34
|
+
.header .meta { color: var(--text-dim); font-size: 13px; }
|
|
35
|
+
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
|
|
36
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
|
37
|
+
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 16px; }
|
|
38
|
+
.card h3 { font-size: 13px; color: var(--text-dim); text-transform: uppercase; margin-bottom: 12px; letter-spacing: 0.5px; }
|
|
39
|
+
.stat { font-size: 28px; font-weight: 700; }
|
|
40
|
+
.stat.green { color: var(--green); }
|
|
41
|
+
.stat.red { color: var(--red); }
|
|
42
|
+
.stat.yellow { color: var(--yellow); }
|
|
43
|
+
table { width: 100%; border-collapse: collapse; }
|
|
44
|
+
th { text-align: left; padding: 10px 12px; color: var(--text-dim); font-size: 12px; text-transform: uppercase; border-bottom: 2px solid var(--border); letter-spacing: 0.5px; }
|
|
45
|
+
td { padding: 10px 12px; border-bottom: 1px solid var(--border); font-size: 14px; font-family: monospace; }
|
|
46
|
+
tr:hover td { background: rgba(88, 166, 255, 0.05); }
|
|
47
|
+
.status { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; }
|
|
48
|
+
.status.online { background: rgba(63, 185, 80, 0.15); color: var(--green); }
|
|
49
|
+
.status.stopped { background: rgba(139, 148, 158, 0.15); color: var(--text-dim); }
|
|
50
|
+
.status.errored { background: rgba(248, 81, 73, 0.15); color: var(--red); }
|
|
51
|
+
.status.launching, .status.waiting-restart { background: rgba(210, 153, 34, 0.15); color: var(--yellow); }
|
|
52
|
+
.btn { padding: 6px 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); color: var(--text); cursor: pointer; font-size: 12px; transition: all 0.2s; }
|
|
53
|
+
.btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
54
|
+
.btn.danger:hover { border-color: var(--red); color: var(--red); }
|
|
55
|
+
.btn.success:hover { border-color: var(--green); color: var(--green); }
|
|
56
|
+
.actions { display: flex; gap: 6px; }
|
|
57
|
+
.chart-container { height: 200px; position: relative; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 16px; margin-bottom: 24px; }
|
|
58
|
+
.chart-title { font-size: 13px; color: var(--text-dim); text-transform: uppercase; margin-bottom: 8px; }
|
|
59
|
+
canvas { width: 100% !important; height: 160px !important; }
|
|
60
|
+
.logs-panel { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 16px; margin-bottom: 24px; }
|
|
61
|
+
.logs-panel h3 { font-size: 13px; color: var(--text-dim); text-transform: uppercase; margin-bottom: 12px; }
|
|
62
|
+
.log-output { background: #000; border-radius: 4px; padding: 12px; font-family: monospace; font-size: 12px; max-height: 400px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; color: var(--text); }
|
|
63
|
+
.log-output .err { color: var(--red); }
|
|
64
|
+
.log-output .timestamp { color: var(--text-dim); }
|
|
65
|
+
.tabs { display: flex; gap: 0; margin-bottom: 16px; }
|
|
66
|
+
.tab { padding: 8px 16px; background: var(--surface); border: 1px solid var(--border); cursor: pointer; font-size: 13px; color: var(--text-dim); transition: all 0.2s; }
|
|
67
|
+
.tab:first-child { border-radius: 6px 0 0 6px; }
|
|
68
|
+
.tab:last-child { border-radius: 0 6px 6px 0; }
|
|
69
|
+
.tab.active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
70
|
+
.system-info { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
|
|
71
|
+
.sys-stat { text-align: center; }
|
|
72
|
+
.sys-stat .label { font-size: 11px; color: var(--text-dim); text-transform: uppercase; }
|
|
73
|
+
.sys-stat .value { font-size: 18px; font-weight: 600; margin-top: 4px; }
|
|
74
|
+
.progress-bar { height: 6px; background: var(--border); border-radius: 3px; margin-top: 6px; overflow: hidden; }
|
|
75
|
+
.progress-bar .fill { height: 100%; border-radius: 3px; transition: width 0.5s; }
|
|
76
|
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
77
|
+
.live-indicator { display: inline-block; width: 8px; height: 8px; background: var(--green); border-radius: 50%; margin-right: 8px; animation: pulse 2s infinite; }
|
|
78
|
+
</style>
|
|
79
|
+
</head>
|
|
80
|
+
<body>
|
|
81
|
+
<div class="header">
|
|
82
|
+
<h1>⚡ BM2 Dashboard</h1>
|
|
83
|
+
<div class="meta"><span class="live-indicator"></span>Live • <span id="update-time">-</span></div>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="container">
|
|
86
|
+
<div class="grid" id="stats-grid"></div>
|
|
87
|
+
<div class="chart-container">
|
|
88
|
+
<div class="chart-title">CPU & Memory Over Time</div>
|
|
89
|
+
<canvas id="chart"></canvas>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="card" style="margin-bottom: 24px;">
|
|
92
|
+
<h3>System</h3>
|
|
93
|
+
<div class="system-info" id="system-info"></div>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="card" style="margin-bottom: 24px;">
|
|
96
|
+
<h3>Processes</h3>
|
|
97
|
+
<table>
|
|
98
|
+
<thead>
|
|
99
|
+
<tr>
|
|
100
|
+
<th>ID</th><th>Name</th><th>Status</th><th>PID</th>
|
|
101
|
+
<th>CPU</th><th>Memory</th><th>Restarts</th><th>Uptime</th>
|
|
102
|
+
<th>Actions</th>
|
|
103
|
+
</tr>
|
|
104
|
+
</thead>
|
|
105
|
+
<tbody id="process-table"></tbody>
|
|
106
|
+
</table>
|
|
107
|
+
</div>
|
|
108
|
+
<div class="logs-panel">
|
|
109
|
+
<h3>Logs</h3>
|
|
110
|
+
<div class="tabs" id="log-tabs"></div>
|
|
111
|
+
<div class="log-output" id="log-output">Select a process to view logs</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
<script>
|
|
115
|
+
const WS_URL = location.origin.replace('http','ws') + '/ws';
|
|
116
|
+
let ws;
|
|
117
|
+
let chartData = { labels: [], cpu: [], memory: [] };
|
|
118
|
+
let selectedLogProcess = null;
|
|
119
|
+
|
|
120
|
+
function formatBytes(b) {
|
|
121
|
+
if (!b) return '0 B';
|
|
122
|
+
const u = ['B','KB','MB','GB'];
|
|
123
|
+
const i = Math.floor(Math.log(b)/Math.log(1024));
|
|
124
|
+
return (b/Math.pow(1024,i)).toFixed(1)+' '+u[i];
|
|
125
|
+
}
|
|
126
|
+
function formatUptime(ms) {
|
|
127
|
+
const s = Math.floor(ms/1000), m = Math.floor(s/60), h = Math.floor(m/60), d = Math.floor(h/24);
|
|
128
|
+
if (d>0) return d+'d '+h%24+'h';
|
|
129
|
+
if (h>0) return h+'h '+m%60+'m';
|
|
130
|
+
if (m>0) return m+'m '+s%60+'s';
|
|
131
|
+
return s+'s';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function connect() {
|
|
135
|
+
ws = new WebSocket(WS_URL);
|
|
136
|
+
ws.onmessage = (e) => {
|
|
137
|
+
const data = JSON.parse(e.data);
|
|
138
|
+
if (data.type === 'state') render(data.data);
|
|
139
|
+
if (data.type === 'logs') renderLogs(data.data);
|
|
140
|
+
};
|
|
141
|
+
ws.onclose = () => setTimeout(connect, 2000);
|
|
142
|
+
ws.onerror = () => ws.close();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function send(type, data) { ws?.send(JSON.stringify({type,data})); }
|
|
146
|
+
|
|
147
|
+
function render(state) {
|
|
148
|
+
const { processes, metrics } = state;
|
|
149
|
+
document.getElementById('update-time').textContent = new Date().toLocaleTimeString();
|
|
150
|
+
|
|
151
|
+
// Stats cards
|
|
152
|
+
const online = processes.filter(p => p.status==='online').length;
|
|
153
|
+
const errored = processes.filter(p => p.status==='errored').length;
|
|
154
|
+
const totalMem = processes.reduce((s,p) => s+p.monit.memory, 0);
|
|
155
|
+
const totalCpu = processes.reduce((s,p) => s+p.monit.cpu, 0);
|
|
156
|
+
|
|
157
|
+
document.getElementById('stats-grid').innerHTML = \`
|
|
158
|
+
<div class="card"><h3>Online</h3><div class="stat green">\${online}</div></div>
|
|
159
|
+
<div class="card"><h3>Errored</h3><div class="stat red">\${errored}</div></div>
|
|
160
|
+
<div class="card"><h3>Total CPU</h3><div class="stat">\${totalCpu.toFixed(1)}%</div></div>
|
|
161
|
+
<div class="card"><h3>Total Memory</h3><div class="stat">\${formatBytes(totalMem)}</div></div>
|
|
162
|
+
\`;
|
|
163
|
+
|
|
164
|
+
// System info
|
|
165
|
+
if (metrics?.system) {
|
|
166
|
+
const sys = metrics.system;
|
|
167
|
+
const memPct = ((sys.totalMemory - sys.freeMemory) / sys.totalMemory * 100).toFixed(1);
|
|
168
|
+
document.getElementById('system-info').innerHTML = \`
|
|
169
|
+
<div class="sys-stat"><div class="label">Platform</div><div class="value">\${sys.platform}</div></div>
|
|
170
|
+
<div class="sys-stat"><div class="label">CPUs</div><div class="value">\${sys.cpuCount}</div></div>
|
|
171
|
+
<div class="sys-stat"><div class="label">Load (1m)</div><div class="value">\${sys.loadAvg[0].toFixed(2)}</div></div>
|
|
172
|
+
<div class="sys-stat">
|
|
173
|
+
<div class="label">Memory</div><div class="value">\${memPct}%</div>
|
|
174
|
+
<div class="progress-bar"><div class="fill" style="width:\${memPct}%;background:\${memPct>80?'var(--red)':memPct>60?'var(--yellow)':'var(--green)'}"></div></div>
|
|
175
|
+
</div>
|
|
176
|
+
\`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Chart data
|
|
180
|
+
const now = new Date().toLocaleTimeString();
|
|
181
|
+
chartData.labels.push(now);
|
|
182
|
+
chartData.cpu.push(totalCpu);
|
|
183
|
+
chartData.memory.push(totalMem / 1024 / 1024);
|
|
184
|
+
if (chartData.labels.length > 60) {
|
|
185
|
+
chartData.labels.shift(); chartData.cpu.shift(); chartData.memory.shift();
|
|
186
|
+
}
|
|
187
|
+
drawChart();
|
|
188
|
+
|
|
189
|
+
// Process table
|
|
190
|
+
document.getElementById('process-table').innerHTML = processes.map(p => \`
|
|
191
|
+
<tr>
|
|
192
|
+
<td>\${p.pm_id}</td>
|
|
193
|
+
<td>\${p.name}</td>
|
|
194
|
+
<td><span class="status \${p.status}">\${p.status}</span></td>
|
|
195
|
+
<td>\${p.pid||'-'}</td>
|
|
196
|
+
<td>\${p.monit.cpu.toFixed(1)}%</td>
|
|
197
|
+
<td>\${formatBytes(p.monit.memory)}</td>
|
|
198
|
+
<td>\${p.pm2_env.restart_time}</td>
|
|
199
|
+
<td>\${p.status==='online' ? formatUptime(Date.now()-p.pm2_env.pm_uptime) : '-'}</td>
|
|
200
|
+
<td class="actions">
|
|
201
|
+
<button class="btn success" onclick="send('restart',{target:'\${p.pm_id}'})">↻</button>
|
|
202
|
+
<button class="btn danger" onclick="send('stop',{target:'\${p.pm_id}'})">■</button>
|
|
203
|
+
<button class="btn" onclick="viewLogs(\${p.pm_id},'\${p.name}')">📋</button>
|
|
204
|
+
</td>
|
|
205
|
+
</tr>
|
|
206
|
+
\`).join('');
|
|
207
|
+
|
|
208
|
+
// Log tabs
|
|
209
|
+
document.getElementById('log-tabs').innerHTML = processes.map(p => \`
|
|
210
|
+
<div class="tab \${selectedLogProcess===p.pm_id?'active':''}" onclick="viewLogs(\${p.pm_id},'\${p.name}')">\${p.name}</div>
|
|
211
|
+
\`).join('');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function viewLogs(id, name) {
|
|
215
|
+
selectedLogProcess = id;
|
|
216
|
+
send('getLogs', { target: id, lines: 50 });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function renderLogs(logs) {
|
|
220
|
+
if (!logs || !logs.length) return;
|
|
221
|
+
const el = document.getElementById('log-output');
|
|
222
|
+
let html = '';
|
|
223
|
+
for (const log of logs) {
|
|
224
|
+
if (log.out) html += log.out.split('\\n').map(l => {
|
|
225
|
+
const m = l.match(/^\\[([^\\]]+)\\]/);
|
|
226
|
+
return m ? '<span class="timestamp">['+m[1]+']</span>'+l.slice(m[0].length) : l;
|
|
227
|
+
}).join('\\n');
|
|
228
|
+
if (log.err) html += log.err.split('\\n').map(l => '<span class="err">'+l+'</span>').join('\\n');
|
|
229
|
+
}
|
|
230
|
+
el.innerHTML = html || 'No logs available';
|
|
231
|
+
el.scrollTop = el.scrollHeight;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function drawChart() {
|
|
235
|
+
const canvas = document.getElementById('chart');
|
|
236
|
+
const ctx = canvas.getContext('2d');
|
|
237
|
+
const w = canvas.offsetWidth, h = 160;
|
|
238
|
+
canvas.width = w * 2; canvas.height = h * 2;
|
|
239
|
+
ctx.scale(2, 2);
|
|
240
|
+
ctx.clearRect(0, 0, w, h);
|
|
241
|
+
|
|
242
|
+
const len = chartData.labels.length;
|
|
243
|
+
if (len < 2) return;
|
|
244
|
+
|
|
245
|
+
const maxCpu = Math.max(...chartData.cpu, 1);
|
|
246
|
+
const maxMem = Math.max(...chartData.memory, 1);
|
|
247
|
+
const stepX = w / (len - 1);
|
|
248
|
+
|
|
249
|
+
// Grid
|
|
250
|
+
ctx.strokeStyle = '#30363d'; ctx.lineWidth = 0.5;
|
|
251
|
+
for (let i = 0; i < 4; i++) {
|
|
252
|
+
const y = h * i / 4;
|
|
253
|
+
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// CPU line
|
|
257
|
+
ctx.strokeStyle = '#58a6ff'; ctx.lineWidth = 2;
|
|
258
|
+
ctx.beginPath();
|
|
259
|
+
chartData.cpu.forEach((v, i) => {
|
|
260
|
+
const x = i * stepX, y = h - (v / maxCpu) * (h - 20);
|
|
261
|
+
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
|
|
262
|
+
});
|
|
263
|
+
ctx.stroke();
|
|
264
|
+
|
|
265
|
+
// Memory line
|
|
266
|
+
ctx.strokeStyle = '#3fb950'; ctx.lineWidth = 2;
|
|
267
|
+
ctx.beginPath();
|
|
268
|
+
chartData.memory.forEach((v, i) => {
|
|
269
|
+
const x = i * stepX, y = h - (v / maxMem) * (h - 20);
|
|
270
|
+
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
|
|
271
|
+
});
|
|
272
|
+
ctx.stroke();
|
|
273
|
+
|
|
274
|
+
// Legend
|
|
275
|
+
ctx.font = '11px monospace';
|
|
276
|
+
ctx.fillStyle = '#58a6ff'; ctx.fillText('● CPU ' + chartData.cpu[len-1]?.toFixed(1) + '%', 10, 14);
|
|
277
|
+
ctx.fillStyle = '#3fb950'; ctx.fillText('● MEM ' + chartData.memory[len-1]?.toFixed(1) + 'MB', 120, 14);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
connect();
|
|
281
|
+
setInterval(() => { if (ws?.readyState === 1) send('getState', {}); }, 2000);
|
|
282
|
+
</script>
|
|
283
|
+
</body>
|
|
284
|
+
</html>`;
|
|
285
|
+
}
|
package/src/dashboard.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BM2 — Bun Process Manager
|
|
3
|
+
* A production-grade process manager for Bun.
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Fork & cluster execution modes
|
|
7
|
+
* - Auto-restart & crash recovery
|
|
8
|
+
* - Health checks & monitoring
|
|
9
|
+
* - Log management & rotation
|
|
10
|
+
* - Deployment support
|
|
11
|
+
*
|
|
12
|
+
* https://github.com/your-org/bm2
|
|
13
|
+
* License: GPL-3.0-only
|
|
14
|
+
* Author: Zak <zak@maxxpainn.com>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { ProcessManager } from "./process-manager";
|
|
18
|
+
import { getDashboardHTML } from "./dashboard-ui";
|
|
19
|
+
import { DASHBOARD_PORT, METRICS_PORT } from "./constants";
|
|
20
|
+
import type { Server, ServerWebSocket } from "bun";
|
|
21
|
+
|
|
22
|
+
export class Dashboard {
|
|
23
|
+
|
|
24
|
+
private server: Server<unknown> | null = null;
|
|
25
|
+
private metricsServer: Server<unknown> | null = null;
|
|
26
|
+
|
|
27
|
+
private clients: Set<ServerWebSocket<unknown>> = new Set();
|
|
28
|
+
private pm: ProcessManager;
|
|
29
|
+
private updateInterval: ReturnType<typeof setInterval> | null = null;
|
|
30
|
+
|
|
31
|
+
constructor(pm: ProcessManager) {
|
|
32
|
+
this.pm = pm;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
start(port: number = DASHBOARD_PORT, metricsPort: number = METRICS_PORT) {
|
|
36
|
+
// Dashboard + WebSocket server
|
|
37
|
+
this.server = Bun.serve<unknown>({
|
|
38
|
+
port,
|
|
39
|
+
fetch: (req, server) => {
|
|
40
|
+
const url = new URL(req.url);
|
|
41
|
+
|
|
42
|
+
if (url.pathname === "/ws") {
|
|
43
|
+
if (server.upgrade(req, { data: undefined })) return;
|
|
44
|
+
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (url.pathname === "/api/processes") {
|
|
48
|
+
return Response.json(this.pm.list());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (url.pathname === "/api/metrics") {
|
|
52
|
+
const metrics = this.pm.monitor.getLatest();
|
|
53
|
+
return Response.json(metrics);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (url.pathname === "/api/metrics/history") {
|
|
57
|
+
const seconds = parseInt(url.searchParams.get("seconds") || "300");
|
|
58
|
+
return Response.json(this.pm.getMetricsHistory(seconds));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (url.pathname === "/api/prometheus" || url.pathname === "/metrics") {
|
|
62
|
+
return new Response(this.pm.getPrometheusMetrics(), {
|
|
63
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Action endpoints
|
|
68
|
+
if (req.method === "POST") {
|
|
69
|
+
return this.handleAction(url.pathname, req);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Serve dashboard HTML
|
|
73
|
+
return new Response(getDashboardHTML(), {
|
|
74
|
+
headers: { "Content-Type": "text/html" },
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
websocket: {
|
|
78
|
+
open: (ws) => {
|
|
79
|
+
this.clients.add(ws);
|
|
80
|
+
// Send initial state
|
|
81
|
+
const state = {
|
|
82
|
+
processes: this.pm.list(),
|
|
83
|
+
metrics: this.pm.monitor.getLatest(),
|
|
84
|
+
};
|
|
85
|
+
ws.send(JSON.stringify({ type: "state", data: state }));
|
|
86
|
+
},
|
|
87
|
+
message: async (ws, message) => {
|
|
88
|
+
try {
|
|
89
|
+
const msg = JSON.parse(String(message));
|
|
90
|
+
await this.handleWsMessage(ws, msg);
|
|
91
|
+
} catch {}
|
|
92
|
+
},
|
|
93
|
+
close: (ws) => {
|
|
94
|
+
this.clients.delete(ws);
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Separate Prometheus metrics server
|
|
100
|
+
this.metricsServer = Bun.serve({
|
|
101
|
+
port: metricsPort,
|
|
102
|
+
fetch: (req) => {
|
|
103
|
+
const url = new URL(req.url);
|
|
104
|
+
if (url.pathname === "/metrics") {
|
|
105
|
+
return new Response(this.pm.getPrometheusMetrics(), {
|
|
106
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return new Response("BM2 Metrics Server\nGET /metrics for Prometheus format", {
|
|
110
|
+
status: 200,
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Periodic broadcast
|
|
116
|
+
this.updateInterval = setInterval(async () => {
|
|
117
|
+
await this.pm.getMetrics(); // Collect snapshot
|
|
118
|
+
this.broadcast();
|
|
119
|
+
}, 2000);
|
|
120
|
+
|
|
121
|
+
console.log(`[bm2] Dashboard running at http://localhost:${port}`);
|
|
122
|
+
console.log(`[bm2] Prometheus metrics at http://localhost:${metricsPort}/metrics`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private async handleAction(pathname: string, req: Request): Promise<Response> {
|
|
126
|
+
try {
|
|
127
|
+
|
|
128
|
+
const body = (await req.json()) as { target?: string; count?: number };
|
|
129
|
+
|
|
130
|
+
switch (pathname) {
|
|
131
|
+
case "/api/restart":
|
|
132
|
+
return Response.json(await this.pm.restart(body.target || "all"));
|
|
133
|
+
case "/api/stop":
|
|
134
|
+
return Response.json(await this.pm.stop(body.target || "all"));
|
|
135
|
+
case "/api/reload":
|
|
136
|
+
return Response.json(await this.pm.reload(body.target || "all"));
|
|
137
|
+
case "/api/delete":
|
|
138
|
+
return Response.json(await this.pm.del(body.target!));
|
|
139
|
+
case "/api/scale":
|
|
140
|
+
return Response.json(await this.pm.scale(body.target!, body.count!));
|
|
141
|
+
case "/api/flush":
|
|
142
|
+
await this.pm.flushLogs(body.target);
|
|
143
|
+
return Response.json({ success: true });
|
|
144
|
+
default:
|
|
145
|
+
return new Response("Not Found", { status: 404 });
|
|
146
|
+
}
|
|
147
|
+
} catch (err: any) {
|
|
148
|
+
return Response.json({ error: err.message }, { status: 500 });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async handleWsMessage(ws: ServerWebSocket<unknown>, msg: any) {
|
|
153
|
+
switch (msg.type) {
|
|
154
|
+
case "getState": {
|
|
155
|
+
const state = {
|
|
156
|
+
processes: this.pm.list(),
|
|
157
|
+
metrics: this.pm.monitor.getLatest(),
|
|
158
|
+
};
|
|
159
|
+
ws.send(JSON.stringify({ type: "state", data: state }));
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case "getLogs": {
|
|
163
|
+
const logs = await this.pm.getLogs(msg.data.target, msg.data.lines || 50);
|
|
164
|
+
ws.send(JSON.stringify({ type: "logs", data: logs }));
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case "restart":
|
|
168
|
+
await this.pm.restart(msg.data.target);
|
|
169
|
+
break;
|
|
170
|
+
case "stop":
|
|
171
|
+
await this.pm.stop(msg.data.target);
|
|
172
|
+
break;
|
|
173
|
+
case "reload":
|
|
174
|
+
await this.pm.reload(msg.data.target);
|
|
175
|
+
break;
|
|
176
|
+
case "scale":
|
|
177
|
+
await this.pm.scale(msg.data.target, msg.data.count);
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private broadcast() {
|
|
183
|
+
const state = {
|
|
184
|
+
processes: this.pm.list(),
|
|
185
|
+
metrics: this.pm.monitor.getLatest(),
|
|
186
|
+
};
|
|
187
|
+
const message = JSON.stringify({ type: "state", data: state });
|
|
188
|
+
for (const client of this.clients) {
|
|
189
|
+
try {
|
|
190
|
+
client.send(message);
|
|
191
|
+
} catch {
|
|
192
|
+
this.clients.delete(client);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
stop() {
|
|
198
|
+
if (this.updateInterval) clearInterval(this.updateInterval);
|
|
199
|
+
if (this.server) this.server.stop();
|
|
200
|
+
if (this.metricsServer) this.metricsServer.stop();
|
|
201
|
+
this.clients.clear();
|
|
202
|
+
}
|
|
203
|
+
}
|
package/src/deploy.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BM2 — Bun Process Manager
|
|
3
|
+
* A production-grade process manager for Bun.
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Fork & cluster execution modes
|
|
7
|
+
* - Auto-restart & crash recovery
|
|
8
|
+
* - Health checks & monitoring
|
|
9
|
+
* - Log management & rotation
|
|
10
|
+
* - Deployment support
|
|
11
|
+
*
|
|
12
|
+
* https://github.com/your-org/bm2
|
|
13
|
+
* License: GPL-3.0-only
|
|
14
|
+
* Author: Zak <zak@maxxpainn.com>
|
|
15
|
+
*/
|
|
16
|
+
import type { DeployConfig } from "./types";
|
|
17
|
+
|
|
18
|
+
export class DeployManager {
|
|
19
|
+
async deploy(config: DeployConfig, command?: string): Promise<void> {
|
|
20
|
+
const hosts = Array.isArray(config.host) ? config.host : [config.host];
|
|
21
|
+
const sshOpts = config.ssh_options || "";
|
|
22
|
+
|
|
23
|
+
for (const host of hosts) {
|
|
24
|
+
const target = `${config.user}@${host}`;
|
|
25
|
+
console.log(`\n[bm2] Deploying to ${target}...`);
|
|
26
|
+
|
|
27
|
+
const remotePath = config.path;
|
|
28
|
+
const currentPath = `${remotePath}/current`;
|
|
29
|
+
const sourcePath = `${remotePath}/source`;
|
|
30
|
+
|
|
31
|
+
// Pre-deploy hook
|
|
32
|
+
if (config.preDeploy) {
|
|
33
|
+
console.log(`[bm2] Running pre-deploy: ${config.preDeploy}`);
|
|
34
|
+
await this.localExec(config.preDeploy);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Setup directory structure
|
|
38
|
+
await this.remoteExec(
|
|
39
|
+
target,
|
|
40
|
+
`mkdir -p ${remotePath} ${sourcePath}`,
|
|
41
|
+
sshOpts
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Clone or pull
|
|
45
|
+
const hasRepo = await this.remoteExec(
|
|
46
|
+
target,
|
|
47
|
+
`test -d ${sourcePath}/.git && echo "yes" || echo "no"`,
|
|
48
|
+
sshOpts
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (hasRepo.trim() === "yes") {
|
|
52
|
+
await this.remoteExec(
|
|
53
|
+
target,
|
|
54
|
+
`cd ${sourcePath} && git fetch --all && git reset --hard ${config.ref}`,
|
|
55
|
+
sshOpts
|
|
56
|
+
);
|
|
57
|
+
} else {
|
|
58
|
+
await this.remoteExec(
|
|
59
|
+
target,
|
|
60
|
+
`git clone ${config.repo} ${sourcePath} && cd ${sourcePath} && git checkout ${config.ref}`,
|
|
61
|
+
sshOpts
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Create release
|
|
66
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
67
|
+
const releasePath = `${remotePath}/releases/${timestamp}`;
|
|
68
|
+
|
|
69
|
+
await this.remoteExec(
|
|
70
|
+
target,
|
|
71
|
+
`mkdir -p ${remotePath}/releases && cp -r ${sourcePath} ${releasePath}`,
|
|
72
|
+
sshOpts
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Symlink current
|
|
76
|
+
await this.remoteExec(
|
|
77
|
+
target,
|
|
78
|
+
`rm -f ${currentPath} && ln -s ${releasePath} ${currentPath}`,
|
|
79
|
+
sshOpts
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Post-deploy hook
|
|
83
|
+
if (config.postDeploy) {
|
|
84
|
+
console.log(`[bm2] Running post-deploy: ${config.postDeploy}`);
|
|
85
|
+
const envStr = config.env
|
|
86
|
+
? Object.entries(config.env)
|
|
87
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
88
|
+
.join(" ")
|
|
89
|
+
: "";
|
|
90
|
+
await this.remoteExec(
|
|
91
|
+
target,
|
|
92
|
+
`cd ${currentPath} && ${envStr} ${config.postDeploy}`,
|
|
93
|
+
sshOpts
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Cleanup old releases (keep last 5)
|
|
98
|
+
await this.remoteExec(
|
|
99
|
+
target,
|
|
100
|
+
`cd ${remotePath}/releases && ls -dt */ | tail -n +6 | xargs rm -rf`,
|
|
101
|
+
sshOpts
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
console.log(`[bm2] ✓ Deploy to ${target} complete`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async setup(config: DeployConfig): Promise<void> {
|
|
109
|
+
const hosts = Array.isArray(config.host) ? config.host : [config.host];
|
|
110
|
+
const sshOpts = config.ssh_options || "";
|
|
111
|
+
|
|
112
|
+
for (const host of hosts) {
|
|
113
|
+
const target = `${config.user}@${host}`;
|
|
114
|
+
console.log(`[bm2] Setting up ${target}...`);
|
|
115
|
+
|
|
116
|
+
await this.remoteExec(
|
|
117
|
+
target,
|
|
118
|
+
`mkdir -p ${config.path} ${config.path}/releases ${config.path}/source ${config.path}/shared`,
|
|
119
|
+
sshOpts
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (config.preSetup) {
|
|
123
|
+
await this.remoteExec(target, config.preSetup, sshOpts);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Clone repo
|
|
127
|
+
await this.remoteExec(
|
|
128
|
+
target,
|
|
129
|
+
`git clone ${config.repo} ${config.path}/source && cd ${config.path}/source && git checkout ${config.ref}`,
|
|
130
|
+
sshOpts
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (config.postSetup) {
|
|
134
|
+
await this.remoteExec(
|
|
135
|
+
target,
|
|
136
|
+
`cd ${config.path}/source && ${config.postSetup}`,
|
|
137
|
+
sshOpts
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.log(`[bm2] ✓ Setup complete for ${target}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private async remoteExec(target: string, command: string, sshOpts: string): Promise<string> {
|
|
146
|
+
const args = ["ssh"];
|
|
147
|
+
if (sshOpts) args.push(...sshOpts.split(" "));
|
|
148
|
+
args.push(target, command);
|
|
149
|
+
|
|
150
|
+
const proc = Bun.spawn(args, {
|
|
151
|
+
stdout: "pipe",
|
|
152
|
+
stderr: "pipe",
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const stdout = await new Response(proc.stdout).text();
|
|
156
|
+
const stderr = await new Response(proc.stderr).text();
|
|
157
|
+
const exitCode = await proc.exited;
|
|
158
|
+
|
|
159
|
+
if (exitCode !== 0 && stderr) {
|
|
160
|
+
console.error(`[bm2] Remote error: ${stderr}`);
|
|
161
|
+
}
|
|
162
|
+
if (stdout.trim()) {
|
|
163
|
+
console.log(stdout.trim());
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return stdout;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private async localExec(command: string): Promise<string> {
|
|
170
|
+
const proc = Bun.spawn(["sh", "-c", command], {
|
|
171
|
+
stdout: "pipe",
|
|
172
|
+
stderr: "pipe",
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const stdout = await new Response(proc.stdout).text();
|
|
176
|
+
const stderr = await new Response(proc.stderr).text();
|
|
177
|
+
const exitCode = await proc.exited;
|
|
178
|
+
|
|
179
|
+
if (exitCode !== 0 && stderr) {
|
|
180
|
+
console.error(`[bm2] Local error: ${stderr}`);
|
|
181
|
+
}
|
|
182
|
+
if (stdout.trim()) {
|
|
183
|
+
console.log(stdout.trim());
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return stdout;
|
|
187
|
+
}
|
|
188
|
+
}
|