bm2 1.0.33 → 1.0.35

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
@@ -451,43 +451,66 @@ bm2 reset all
451
451
 
452
452
  Cluster mode spawns multiple instances of your application, each running in its own process. This is ideal for CPU-bound workloads and for taking full advantage of multi-core servers.
453
453
 
454
- ```
454
+ ```bash
455
455
  bm2 start server.ts --name api --instances max
456
- ```
457
456
 
458
457
  ```
458
+
459
+ ```bash
459
460
  bm2 start server.ts --name api --instances 4
460
- ```
461
461
 
462
462
  ```
463
+
464
+ ```bash
463
465
  bm2 start server.ts --name api --instances 4 --port 3000
466
+
464
467
  ```
465
468
 
469
+ #### ⚠️ Current Status & Limitations
470
+
471
+ While `bm2` provides the orchestration for clustering, please note that **Bun’s native cluster implementation is currently limited by the underlying OS:**
472
+
473
+ * **Linux Only:** Port sharing via `reusePort` is only fully supported on **Linux**.
474
+ * **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
+
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.
477
+
478
+
479
+ #### Environment Variables
480
+
466
481
  Each cluster worker receives the following environment variables:
467
482
 
468
483
  | Variable | Description |
469
- |---|---|
484
+ | --- | --- |
470
485
  | `BM2_CLUSTER` | Set to `"true"` in cluster mode |
471
486
  | `BM2_WORKER_ID` | Zero-indexed worker ID |
472
487
  | `BM2_INSTANCES` | Total number of instances |
473
488
  | `NODE_APP_INSTANCE` | Same as `BM2_WORKER_ID` (PM2 compatibility) |
474
489
  | `PORT` | `basePort + workerIndex` (if `--port` is specified) |
475
490
 
476
- Example application using cluster-aware port binding:
491
+ ---
477
492
 
478
- ```
493
+ #### Example: Cluster-Aware Port Binding
494
+
495
+ To enable clustering in Bun, you must explicitly set `reusePort: true`. This allows multiple processes to listen on the same port (on supported OSs).
496
+
497
+ ```typescript
479
498
  // server.ts
480
499
  const workerId = parseInt(process.env.BM2_WORKER_ID || "0");
481
500
  const port = parseInt(process.env.PORT || "3000");
482
501
 
483
502
  Bun.serve({
484
503
  port,
504
+ // Share the same port across multiple processes
505
+ // This is the important part!
506
+ reusePort: true,
485
507
  fetch(req) {
486
508
  return new Response(`Hello from worker ${workerId} on port ${port}`);
487
509
  },
488
510
  });
489
511
 
490
512
  console.log(`Worker ${workerId} listening on :${port}`);
513
+
491
514
  ```
492
515
 
493
516
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bm2",
3
- "version": "1.0.33",
3
+ "version": "1.0.35",
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",
package/src/daemon.ts CHANGED
@@ -35,6 +35,11 @@ const pm = new ProcessManager();
35
35
  const dashboard = new Dashboard(pm);
36
36
  const moduleManager = new ModuleManager(pm);
37
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
+
38
43
  // Clean up existing socket
39
44
  if (existsSync(DAEMON_SOCKET)) {
40
45
  try { unlinkSync(DAEMON_SOCKET); } catch {}
@@ -223,8 +228,13 @@ async function handleMessage(msg: DaemonMessage): Promise<DaemonResponse> {
223
228
  default:
224
229
  return { type: "error", error: `Unknown command: ${msg.type}`, success: false, id: msg.id };
225
230
  }
226
- } catch (err: any) {
227
- return { type: "error", error: err.message, success: false, id: msg.id };
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 };
228
238
  }
229
239
  }
230
240
 
package/src/index.ts CHANGED
@@ -41,6 +41,7 @@ import type {
41
41
  } from "./types";
42
42
  import { statusColor } from "./colors";
43
43
  import { liveWatchProcess, printProcessTable } from "./process-table";
44
+ import { exists } from "fs/promises";
44
45
 
45
46
  // ---------------------------------------------------------------------------
46
47
  // Ensure directory structure exists
@@ -51,6 +52,7 @@ ensureDirs();
51
52
  // Daemon communication helpers
52
53
  // ---------------------------------------------------------------------------
53
54
 
55
+
54
56
  function isDaemonRunning(): boolean {
55
57
  if (!existsSync(DAEMON_PID_FILE)) return false;
56
58
  try {
@@ -98,6 +100,25 @@ async function startDaemon(): Promise<void> {
98
100
  }
99
101
  }
100
102
 
103
+ async function stopDaemon(): Promise<void> {
104
+ try {
105
+
106
+ if (!isDaemonRunning()) return;
107
+
108
+ const pidText = await Bun.file(DAEMON_PID_FILE).text();
109
+ const pid = Number(pidText);
110
+
111
+ process.kill(pid, "SIGTERM"); // graceful stop
112
+
113
+ console.error("Daemon stopped");
114
+
115
+ // cleanup
116
+ await Bun.write(DAEMON_PID_FILE, "");
117
+ } catch (err) {
118
+ console.error("Failed to stop daemon:", err);
119
+ }
120
+ }
121
+
101
122
  async function sendToDaemon(msg: DaemonMessage): Promise<DaemonResponse> {
102
123
 
103
124
  //start the daemon
@@ -143,8 +164,10 @@ async function sendToDaemon(msg: DaemonMessage): Promise<DaemonResponse> {
143
164
  async function loadEcosystemConfig(filePath: string): Promise<EcosystemConfig> {
144
165
 
145
166
  const abs = resolve(filePath);
167
+
168
+ const file = Bun.file(abs);
146
169
 
147
- if (!existsSync(abs)) {
170
+ if (!(await file.exists())) {
148
171
  throw new Error(`Ecosystem file not found: ${abs}`);
149
172
  }
150
173
 
@@ -153,7 +176,7 @@ async function loadEcosystemConfig(filePath: string): Promise<EcosystemConfig> {
153
176
  let config;
154
177
 
155
178
  if (ext === ".json") {
156
- config = (await Bun.file(abs).json()) as EcosystemConfig;
179
+ config = (await file.json()) as EcosystemConfig;
157
180
  } else {
158
181
  // .ts, .js, .mjs — dynamic import
159
182
  const mod = await import(abs);
@@ -362,8 +385,11 @@ async function cmdStart(args: string[]) {
362
385
  opts.script = resolve(scriptOrConfig);
363
386
 
364
387
  const cwd = path.dirname(opts.script);
365
-
366
- const res = await sendToDaemon({ type: "start", data: { config: opts, cwd } });
388
+
389
+ opts.cwd = cwd;
390
+
391
+ const res = await sendToDaemon({ type: "start", data: opts });
392
+
367
393
  if (!res.success) {
368
394
  console.error(colorize(`Error: ${res.error}`, "red"));
369
395
  process.exit(1);
@@ -697,7 +723,8 @@ async function cmdDeploy(args: string[]) {
697
723
  process.exit(1);
698
724
  }
699
725
 
700
- const {config, cwd } = await loadEcosystemConfig(configFile);
726
+ const config = await loadEcosystemConfig(configFile);
727
+
701
728
  if (!config.deploy || !config.deploy[environment]) {
702
729
  console.error(colorize(`Deploy environment "${environment}" not found in config`, "red"));
703
730
  process.exit(1);
@@ -846,29 +873,42 @@ async function cmdModule(args: string[]) {
846
873
  }
847
874
 
848
875
  async function cmdDaemon(args: string[]) {
876
+
877
+
878
+ const daemonStatus = () => {
879
+ if (isDaemonRunning()) {
880
+ console.log(colorize("running", "green"));
881
+ } else {
882
+ console.error(colorize("stopped", "red"));
883
+ }
884
+
885
+ process.exit(1);
886
+ }
887
+
849
888
  const subCmd = args[0];
850
- let type;
851
889
 
852
890
  switch (subCmd) {
891
+ case "status":
892
+ daemonStatus();
893
+ break;
894
+ case "start":
895
+ await startDaemon();
896
+ process.exit(1);
897
+ break
898
+ case "stop":
899
+ await stopDaemon();
900
+ process.exit(1);
901
+ break;
853
902
  case "reload":
854
- type = "daemonReload"
903
+ await stopDaemon();
904
+ await startDaemon();
905
+ process.exit(1);
855
906
  break;
856
907
  default:
857
- console.error(colorize("Usage: bm2 daemon <reload>", "red"));
908
+ console.error(colorize("Usage: bm2 daemon <status|start|stop|reload>", "red"));
858
909
  process.exit(1);
859
910
  }
860
-
861
- const res = await sendToDaemon({ type });
862
911
 
863
- if (res?.error) {
864
- console.error(colorize(`Error: ${res.error}`, "red"));
865
- process.exit(1);
866
- }
867
-
868
- console.log(colorize(res.data, "green"));
869
-
870
- process.exit(1);
871
-
872
912
  }
873
913
 
874
914
  async function cmdPrometheus() {
@@ -66,11 +66,12 @@ import path from "path";
66
66
  options.script = path.isAbsolute(options.script)
67
67
  ? options.script
68
68
  : path.join(options.cwd!, options.script);
69
-
69
+
70
+
70
71
  if (!(await Bun.file(options.script).exists())) {
71
72
  throw new Error(`Script not found: ${options.script}`);
72
- }
73
-
73
+ }
74
+
74
75
  if (isCluster) {
75
76
  // In cluster mode, each instance is a separate container
76
77
  for (let i = 0; i < resolvedInstances; i++) {
@@ -94,6 +95,7 @@ import path from "path";
94
95
  await container.start();
95
96
  states.push(container.getState());
96
97
  }
98
+
97
99
  } else {
98
100
  const id = this.nextId++;
99
101
  const name =
@@ -103,8 +105,11 @@ import path from "path";
103
105
 
104
106
  const config = this.buildConfig(id, name, options, 1, 0);
105
107
  const container = new ProcessContainer(
106
- id, config, this.logManager, this.clusterManager,
107
- this.healthChecker, this.cronManager
108
+ id, config,
109
+ this.logManager,
110
+ this.clusterManager,
111
+ this.healthChecker,
112
+ this.cronManager
108
113
  );
109
114
 
110
115
  this.processes.set(id, container);
@@ -113,7 +118,7 @@ import path from "path";
113
118
  }
114
119
 
115
120
  return states;
116
- }
121
+ }
117
122
 
118
123
  private buildConfig(
119
124
  id: number,