bm2 1.0.35 → 1.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  # ⚡ BM2
3
2
 
4
3
  **A blazing-fast, full-featured process manager built entirely on Bun native APIs.**
@@ -34,6 +33,7 @@ The modern PM2 replacement — zero Node.js dependencies, pure Bun performance.
34
33
  - [Startup Scripts](#startup-scripts)
35
34
  - [Modules](#modules)
36
35
  - [Daemon Control](#daemon-control)
36
+ - [Foreground Mode (Docker & Containers)](#foreground-mode-docker--containers)
37
37
  - [Configuration Reference](#configuration-reference)
38
38
  - [Ecosystem File Format](#ecosystem-file-format)
39
39
  - [Process Options](#process-options)
@@ -88,6 +88,8 @@ BM2 replaces PM2's Node.js internals with Bun-native APIs. It uses `Bun.spawn` f
88
88
 
89
89
  **Zero-Downtime Reload** — Graceful reload cycles through instances sequentially, starting the new process before stopping the old one, ensuring your application never drops a request.
90
90
 
91
+ **Foreground / No-Daemon Mode** — Run BM2 in blocking foreground mode without spawning a background daemon. Designed for containerized environments like Docker, Kubernetes, and any platform that expects PID 1 to remain in the foreground.
92
+
91
93
  **Real-Time Web Dashboard** — A built-in dark-themed web dashboard with live WebSocket updates, CPU/memory charts, process control buttons, and a log viewer. No external dependencies.
92
94
 
93
95
  **Prometheus Metrics** — A dedicated metrics endpoint exports process and system telemetry in Prometheus exposition format, ready for scraping by Prometheus and visualization in Grafana.
@@ -283,6 +285,14 @@ bm2 start server.ts --name api --wait-ready --listen-timeout 10000
283
285
  | `--health-check-interval <ms>` | Probe interval | `30000` |
284
286
  | `--health-check-timeout <ms>` | Probe timeout | `5000` |
285
287
  | `--health-check-max-fails <n>` | Failures before restart | `3` |
288
+ | `--no-daemon`, `-d` | Run in foreground without a daemon (blocks) | `false` |
289
+
290
+ > **Flags are position-independent.** `--no-daemon` (and all other flags) may appear anywhere relative to the script path:
291
+ > ```
292
+ > bm2 start --no-daemon app.ts
293
+ > bm2 start app.ts --no-daemon
294
+ > bm2 start --name api --no-daemon app.ts --watch
295
+ > ```
286
296
 
287
297
  ---
288
298
 
@@ -415,7 +425,6 @@ bm2 list
415
425
 
416
426
  # List processes with live updates
417
427
  bm2 list --live
418
-
419
428
  ```
420
429
 
421
430
  ## Notes
@@ -453,27 +462,24 @@ Cluster mode spawns multiple instances of your application, each running in its
453
462
 
454
463
  ```bash
455
464
  bm2 start server.ts --name api --instances max
456
-
457
465
  ```
458
466
 
459
467
  ```bash
460
468
  bm2 start server.ts --name api --instances 4
461
-
462
469
  ```
463
470
 
464
471
  ```bash
465
472
  bm2 start server.ts --name api --instances 4 --port 3000
466
-
467
473
  ```
468
474
 
469
475
  #### ⚠️ Current Status & Limitations
470
476
 
471
- While `bm2` provides the orchestration for clustering, please note that **Buns native cluster implementation is currently limited by the underlying OS:**
477
+ While `bm2` provides the orchestration for clustering, please note that **Bun's native cluster implementation is currently limited by the underlying OS:**
472
478
 
473
479
  * **Linux Only:** Port sharing via `reusePort` is only fully supported on **Linux**.
474
480
  * **macOS & Windows:** Due to OS-level limitations with `SO_REUSEPORT`, these platforms ignore the `reusePort` option. On these systems, clustering may result in "Address already in use" errors if attempting to bind multiple workers to the same port.
475
481
 
476
- `bm2` leverages the native [Bun.serve cluster logic](https://www.google.com/search?q=https://bun.sh/docs/api/http%23cluster) to ensure maximum performance, but it remains subject to the runtime's maturity.
482
+ `bm2` leverages the native [Bun.serve cluster logic](https://bun.sh/docs/api/http#cluster) to ensure maximum performance, but it remains subject to the runtime's maturity.
477
483
 
478
484
 
479
485
  #### Environment Variables
@@ -503,14 +509,13 @@ Bun.serve({
503
509
  port,
504
510
  // Share the same port across multiple processes
505
511
  // This is the important part!
506
- reusePort: true,
512
+ reusePort: true,
507
513
  fetch(req) {
508
514
  return new Response(`Hello from worker ${workerId} on port ${port}`);
509
515
  },
510
516
  });
511
517
 
512
518
  console.log(`Worker ${workerId} listening on :${port}`);
513
-
514
519
  ```
515
520
 
516
521
  ---
@@ -1001,6 +1006,108 @@ bm2 kill
1001
1006
 
1002
1007
  ---
1003
1008
 
1009
+ ## Foreground Mode (Docker & Containers)
1010
+
1011
+ By default, BM2 spawns a background daemon process and returns immediately — ideal for long-running servers. However, containerized environments like **Docker**, **Kubernetes**, and **Railway** expect the entrypoint process to stay in the **foreground**. If BM2 daemonizes and exits, the container stops.
1012
+
1013
+ Use `--no-daemon` (alias `-d`) to run BM2 in **foreground / blocking mode**. In this mode:
1014
+
1015
+ - No background daemon is spawned.
1016
+ - The `bm2 start` process itself stays alive, blocking the terminal (or container).
1017
+ - All managed child processes are supervised in-process.
1018
+ - Auto-restart and crash recovery still work normally.
1019
+ - The process exits only when all child processes stop or a signal (e.g. `SIGTERM`) is received.
1020
+
1021
+ ### Flags
1022
+
1023
+ | Flag | Alias | Description |
1024
+ |---|---|---|
1025
+ | `--no-daemon` | `-d` | Run in foreground without spawning a background daemon |
1026
+
1027
+ ### Usage
1028
+
1029
+ ```bash
1030
+ # Foreground — blocks until the process exits
1031
+ bm2 start --no-daemon server.ts
1032
+
1033
+ # Flag order is flexible — these are all equivalent
1034
+ bm2 start server.ts --no-daemon
1035
+ bm2 start --no-daemon server.ts --name api
1036
+ bm2 start --name api --no-daemon server.ts
1037
+ ```
1038
+
1039
+ ### Docker
1040
+
1041
+ This is the recommended pattern for running BM2 inside a Docker container. The `CMD` instruction should use `--no-daemon` so BM2 stays as PID 1 (or the foreground entrypoint) and Docker can track its lifecycle correctly.
1042
+
1043
+ **Dockerfile**
1044
+
1045
+ ```dockerfile
1046
+ FROM oven/bun:latest
1047
+
1048
+ WORKDIR /app
1049
+
1050
+ COPY package.json bun.lockb ./
1051
+ RUN bun install --frozen-lockfile
1052
+
1053
+ COPY . .
1054
+
1055
+ # Install BM2 globally
1056
+ RUN bun add -g bm2
1057
+
1058
+ # Use --no-daemon so BM2 stays in the foreground
1059
+ CMD ["bm2", "start", "--no-daemon", "./server.ts"]
1060
+ ```
1061
+
1062
+ **With additional options**
1063
+
1064
+ ```dockerfile
1065
+ CMD ["bm2", "start", "--no-daemon", "--name", "api", "--instances", "2", "./server.ts"]
1066
+ ```
1067
+
1068
+ **With an ecosystem file**
1069
+
1070
+ ```dockerfile
1071
+ CMD ["bm2", "start", "--no-daemon", "ecosystem.config.json"]
1072
+ ```
1073
+
1074
+ > **Note:** Ecosystem file support with `--no-daemon` behaves identically to normal mode — all `apps` entries are started and supervised in-process.
1075
+
1076
+ ### Docker Compose
1077
+
1078
+ ```yaml
1079
+ services:
1080
+ api:
1081
+ build: .
1082
+ ports:
1083
+ - "3000:3000"
1084
+ command: ["bm2", "start", "--no-daemon", "./server.ts"]
1085
+ restart: unless-stopped
1086
+ ```
1087
+
1088
+ ### Kubernetes
1089
+
1090
+ ```yaml
1091
+ containers:
1092
+ - name: api
1093
+ image: your-org/api:latest
1094
+ command: ["bm2", "start", "--no-daemon", "./server.ts"]
1095
+ ```
1096
+
1097
+ ### Behaviour Differences vs. Daemon Mode
1098
+
1099
+ | Behaviour | Daemon mode (default) | Foreground mode (`--no-daemon`) |
1100
+ |---|---|---|
1101
+ | CLI returns immediately | ✅ | ❌ — blocks |
1102
+ | Background daemon spawned | ✅ | ❌ |
1103
+ | Unix socket IPC | ✅ | ❌ |
1104
+ | Auto-restart on crash | ✅ | ✅ |
1105
+ | `bm2 list` / `bm2 logs` from another shell | ✅ | ❌ — no daemon to query |
1106
+ | Suitable for Docker / containers | ❌ | ✅ |
1107
+ | Suitable for long-running servers | ✅ | ✅ |
1108
+
1109
+ ---
1110
+
1004
1111
  ## Configuration Reference
1005
1112
 
1006
1113
  ### Ecosystem File Format
@@ -1042,6 +1149,7 @@ The complete set of options available for each entry in the apps array:
1042
1149
  | `sourceMapSupport` | `boolean` | `false` | Enable source map support |
1043
1150
  | `waitReady` | `boolean` | `false` | Wait for process to emit ready signal |
1044
1151
  | `listenTimeout` | `number` | `3000` | Timeout when waiting for ready signal |
1152
+ | `noDaemon` | `boolean` | `false` | Run in foreground without a daemon |
1045
1153
 
1046
1154
  ---
1047
1155
 
@@ -1273,7 +1381,7 @@ groups:
1273
1381
  labels:
1274
1382
  severity: critical
1275
1383
  annotations:
1276
- summary: "Process {{ \$labels.name }} is down"
1384
+ summary: "Process {{ $labels.name }} is down"
1277
1385
 
1278
1386
  - alert: HighRestartRate
1279
1387
  expr: rate(bm2_process_restarts_total[5m]) > 0.1
@@ -1281,7 +1389,7 @@ groups:
1281
1389
  labels:
1282
1390
  severity: warning
1283
1391
  annotations:
1284
- summary: "Process {{ \$labels.name }} is restarting frequently"
1392
+ summary: "Process {{ $labels.name }} is restarting frequently"
1285
1393
 
1286
1394
  - alert: HighMemoryUsage
1287
1395
  expr: bm2_process_memory_bytes > 1e9
@@ -1289,7 +1397,7 @@ groups:
1289
1397
  labels:
1290
1398
  severity: warning
1291
1399
  annotations:
1292
- summary: "Process {{ \$labels.name }} using > 1GB memory"
1400
+ summary: "Process {{ $labels.name }} using > 1GB memory"
1293
1401
  ```
1294
1402
 
1295
1403
  ---
@@ -1885,6 +1993,7 @@ The `ProcessManager` provides the same process management capabilities but runs
1885
1993
  | Module System | `pm2 install` | `bm2 module install` |
1886
1994
  | TypeScript | Requires compilation | Native support |
1887
1995
  | File Watching | `chokidar` | Native `fs.watch` |
1996
+ | Docker / Foreground Mode | `--no-daemon` flag | `--no-daemon` / `-d` flag |
1888
1997
 
1889
1998
  ---
1890
1999
 
@@ -1948,6 +2057,18 @@ bm2 start server.ts --name api --cron "0 3 * * *"
1948
2057
  }
1949
2058
  ```
1950
2059
 
2060
+ ### Docker Container (Foreground Mode)
2061
+
2062
+ ```dockerfile
2063
+ FROM oven/bun:latest
2064
+ WORKDIR /app
2065
+ COPY package.json bun.lockb ./
2066
+ RUN bun install --frozen-lockfile
2067
+ COPY . .
2068
+ RUN bun add -g bm2
2069
+ CMD ["bm2", "start", "--no-daemon", "./server.ts"]
2070
+ ```
2071
+
1951
2072
  ### Full Production Setup
1952
2073
 
1953
2074
  ```
@@ -2121,6 +2242,18 @@ bm2 ping
2121
2242
 
2122
2243
  This returns the daemon PID and uptime. If it doesn't respond, the daemon needs to be restarted.
2123
2244
 
2245
+ ### Container exits immediately
2246
+
2247
+ If your Docker container exits right after starting, you are likely missing `--no-daemon`. Without it, BM2 daemonizes and the foreground process exits, causing Docker to stop the container.
2248
+
2249
+ ```dockerfile
2250
+ # ❌ Wrong — BM2 daemonizes and the container exits
2251
+ CMD ["bm2", "start", "./server.ts"]
2252
+
2253
+ # ✅ Correct — BM2 stays in the foreground
2254
+ CMD ["bm2", "start", "--no-daemon", "./server.ts"]
2255
+ ```
2256
+
2124
2257
  ---
2125
2258
 
2126
2259
  ## File Structure
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bm2",
3
- "version": "1.0.35",
3
+ "version": "1.0.36",
4
4
  "description": "A blazing-fast, full-featured process manager built entirely on Bun native APIs. The modern PM2 replacement — zero Node.js dependencies, pure Bun performance.",
5
5
  "main": "src/api.ts",
6
6
  "module": "src/api.ts",
@@ -0,0 +1,244 @@
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 { Dashboard } from "./dashboard";
19
+ import { ModuleManager } from "./module-manager";
20
+ import {
21
+ DAEMON_SOCKET,
22
+ DAEMON_PID_FILE,
23
+ DASHBOARD_PORT,
24
+ METRICS_PORT,
25
+ } from "./constants";
26
+ import { ensureDirs } from "./utils";
27
+ import { unlinkSync, existsSync } from "fs";
28
+ import type { DaemonMessage, DaemonResponse } from "./types";
29
+ import type { BunRequest, Server } from "bun";
30
+
31
+ ensureDirs();
32
+
33
+ let server: Server<any> | null = null
34
+ const pm = new ProcessManager();
35
+ const dashboard = new Dashboard(pm);
36
+ const moduleManager = new ModuleManager(pm);
37
+
38
+ const args = process.argv.slice(2);
39
+
40
+ // Checks if '--debug' exists anywhere in the arguments
41
+ const debugMode = args.includes('--debug');
42
+
43
+ // Clean up existing socket
44
+ if (existsSync(DAEMON_SOCKET)) {
45
+ try { unlinkSync(DAEMON_SOCKET); } catch {}
46
+ }
47
+
48
+ // Write PID file
49
+ await Bun.write(DAEMON_PID_FILE, String(process.pid));
50
+
51
+ // Load modules
52
+ await moduleManager.loadAll();
53
+
54
+ // Start metric collection
55
+ const metricsInterval = setInterval(() => {
56
+ pm.getMetrics();
57
+ }, 2000);
58
+
59
+
60
+ const handleServerRequests = async (req: Request) => {
61
+
62
+ if (req.method !== "POST") {
63
+ return Response.json(
64
+ { type: "error", error: "Method Not Allowed", success: false },
65
+ { status: 405 }
66
+ )
67
+ }
68
+
69
+ try {
70
+
71
+ const msg: DaemonMessage = await req.json() as DaemonMessage;
72
+
73
+ const response = await handleMessage(msg);
74
+
75
+ return Response.json(response);
76
+
77
+ } catch (err: any) {
78
+ return Response.json(
79
+ { type: "error", error: err.message, success: false },
80
+ { status: 500 }
81
+ );
82
+ }
83
+ };
84
+
85
+ const serverOptions = {
86
+ unix: DAEMON_SOCKET,
87
+ fetch: handleServerRequests
88
+ }
89
+
90
+ async function handleMessage(msg: DaemonMessage): Promise<DaemonResponse> {
91
+ try {
92
+ switch (msg.type) {
93
+ case "start": {
94
+ const states = await pm.start(msg.data);
95
+ return { type: "start", data: states, success: true, id: msg.id };
96
+ }
97
+ case "stop": {
98
+ const states = await pm.stop(msg.data.target);
99
+ return { type: "stop", data: states, success: true, id: msg.id };
100
+ }
101
+ case "restart": {
102
+ const states = await pm.restart(msg.data.target);
103
+ return { type: "restart", data: states, success: true, id: msg.id };
104
+ }
105
+ case "reload": {
106
+ const states = await pm.reload(msg.data.target);
107
+ return { type: "reload", data: states, success: true, id: msg.id };
108
+ }
109
+ case "delete": {
110
+ const states = await pm.del(msg.data.target);
111
+ return { type: "delete", data: states, success: true, id: msg.id };
112
+ }
113
+ case "scale": {
114
+ const states = await pm.scale(msg.data.target, msg.data.count);
115
+ return { type: "scale", data: states, success: true, id: msg.id };
116
+ }
117
+ case "stopAll": {
118
+ const states = await pm.stopAll();
119
+ return { type: "stopAll", data: states, success: true, id: msg.id };
120
+ }
121
+ case "restartAll": {
122
+ const states = await pm.restartAll();
123
+ return { type: "restartAll", data: states, success: true, id: msg.id };
124
+ }
125
+ case "reloadAll": {
126
+ const states = await pm.reloadAll();
127
+ return { type: "reloadAll", data: states, success: true, id: msg.id };
128
+ }
129
+ case "deleteAll": {
130
+ const states = await pm.deleteAll();
131
+ return { type: "deleteAll", data: states, success: true, id: msg.id };
132
+ }
133
+ case "list": {
134
+ return { type: "list", data: pm.list(), success: true, id: msg.id };
135
+ }
136
+ case "describe": {
137
+ return { type: "describe", data: pm.describe(msg.data.target), success: true, id: msg.id };
138
+ }
139
+ case "logs": {
140
+ const logs = await pm.getLogs(msg.data.target, msg.data.lines);
141
+ return { type: "logs", data: logs, success: true, id: msg.id };
142
+ }
143
+ case "flush": {
144
+ await pm.flushLogs(msg.data?.target);
145
+ return { type: "flush", success: true, id: msg.id };
146
+ }
147
+ case "save": {
148
+ await pm.save();
149
+ return { type: "save", success: true, id: msg.id };
150
+ }
151
+ case "resurrect": {
152
+ const states = await pm.resurrect();
153
+ return { type: "resurrect", data: states, success: true, id: msg.id };
154
+ }
155
+ case "ecosystem": {
156
+ const states = await pm.startEcosystem(msg.data);
157
+ return { type: "ecosystem", data: states, success: true, id: msg.id };
158
+ }
159
+ case "signal": {
160
+ await pm.sendSignal(msg.data.target, msg.data.signal);
161
+ return { type: "signal", success: true, id: msg.id };
162
+ }
163
+ case "reset": {
164
+ const states = await pm.reset(msg.data.target);
165
+ return { type: "reset", data: states, success: true, id: msg.id };
166
+ }
167
+ case "metrics": {
168
+ const metrics = await pm.getMetrics();
169
+ return { type: "metrics", data: metrics, success: true, id: msg.id };
170
+ }
171
+ case "metricsHistory": {
172
+ const history = pm.getMetricsHistory(msg.data?.seconds || 300);
173
+ return { type: "metricsHistory", data: history, success: true, id: msg.id };
174
+ }
175
+ case "prometheus": {
176
+ const prom = pm.getPrometheusMetrics();
177
+ return { type: "prometheus", data: prom, success: true, id: msg.id };
178
+ }
179
+ case "dashboard": {
180
+ const port = msg.data?.port || DASHBOARD_PORT;
181
+ const metricsPort = msg.data?.metricsPort || METRICS_PORT;
182
+ dashboard.start(port, metricsPort);
183
+ return { type: "dashboard", data: { port, metricsPort }, success: true, id: msg.id };
184
+ }
185
+ case "dashboardStop": {
186
+ dashboard.stop();
187
+ return { type: "dashboardStop", success: true, id: msg.id };
188
+ }
189
+ case "moduleInstall": {
190
+ const path = await moduleManager.install(msg.data.module);
191
+ return { type: "moduleInstall", data: { path }, success: true, id: msg.id };
192
+ }
193
+ case "moduleUninstall": {
194
+ await moduleManager.uninstall(msg.data.module);
195
+ return { type: "moduleUninstall", success: true, id: msg.id };
196
+ }
197
+
198
+ case "moduleList": {
199
+ return { type: "moduleList", data: moduleManager.list(), success: true, id: msg.id };
200
+ }
201
+
202
+ case "daemonReload": {
203
+ if (!server) {
204
+ server = Bun.serve(serverOptions);
205
+ } else {
206
+ server.reload(serverOptions)
207
+ }
208
+
209
+ return { type: "daemonReload", data: `Daemon reloaded`, success: true, id: msg.id };
210
+ }
211
+
212
+ case "ping": {
213
+ return {
214
+ type: "pong",
215
+ data: { pid: process.pid, uptime: process.uptime() },
216
+ success: true,
217
+ id: msg.id,
218
+ };
219
+ }
220
+
221
+ case "kill": {
222
+ await pm.stopAll();
223
+ dashboard.stop();
224
+ clearInterval(metricsInterval);
225
+ setTimeout(() => process.exit(0), 200);
226
+ return { type: "kill", success: true, id: msg.id };
227
+ }
228
+ default:
229
+ return { type: "error", error: `Unknown command: ${msg.type}`, success: false, id: msg.id };
230
+ }
231
+ } catch (err: Error | any) {
232
+ let error = err.message;
233
+ if (debugMode) {
234
+ error = `Message: ${err.message}\nStack: ${err.stack}`
235
+ console.error(err, err.stack)
236
+ }
237
+ return { type: "error", error, success: false, id: msg.id };
238
+ }
239
+ }
240
+
241
+
242
+ server = Bun.serve(serverOptions);
243
+
244
+ console.log(`Listening on ${server.url}`);