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/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "bm2",
3
+ "version": "1.0.0",
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
+ "main": "src/index.ts",
6
+ "module": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "bin": {
9
+ "bm2": "./src/index.ts"
10
+ },
11
+ "files": [
12
+ "src/",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "start": "bun run src/index.ts",
18
+ "dev": "bun run --watch src/index.ts",
19
+ "test": "bun test",
20
+ "lint": "bun x tsc --noEmit",
21
+ "prepublishOnly": "bun test"
22
+ },
23
+ "keywords": [
24
+ "bun",
25
+ "process-manager",
26
+ "pm2",
27
+ "pm2-alternative",
28
+ "cluster",
29
+ "daemon",
30
+ "production",
31
+ "deployment",
32
+ "monitoring",
33
+ "prometheus",
34
+ "zero-downtime",
35
+ "reload",
36
+ "log-management",
37
+ "health-check",
38
+ "bm2"
39
+ ],
40
+ "author": {
41
+ "name": "MaxxPainn",
42
+ "email": "hello@maxxpainn.com",
43
+ "url": "https://maxxpainn.com"
44
+ },
45
+ "license": "MIT",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/bun-bm2/bm2.git"
49
+ },
50
+ "bugs": {
51
+ "url": "https://github.com/bun-bm2/bm2/issues",
52
+ "email": "hello@maxxpainn.com"
53
+ },
54
+ "homepage": "https://github.com/bun-bm2/bm2#readme",
55
+ "engines": {
56
+ "bun": ">=1.0.0"
57
+ },
58
+ "os": [
59
+ "linux",
60
+ "darwin"
61
+ ],
62
+ "dependencies": {},
63
+ "devDependencies": {
64
+ "@types/bun": "^1.3.9",
65
+ "bun-types": "latest",
66
+ "typescript": "^5.9.3"
67
+ }
68
+ }
@@ -0,0 +1,117 @@
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 { Subprocess } from "bun";
17
+ import type { ProcessDescription } from "./types";
18
+ import { getCpuCount } from "./utils";
19
+
20
+ export class ClusterManager {
21
+ private workers: Map<number, Map<number, Subprocess>> = new Map();
22
+
23
+ resolveInstances(instances: number | string | undefined): number {
24
+ if (instances === undefined || instances === 0) return 1;
25
+ if (typeof instances === "string") {
26
+ if (instances === "max" || instances === "-1") return getCpuCount();
27
+ return parseInt(instances) || 1;
28
+ }
29
+ if (instances === -1) return getCpuCount();
30
+ return instances;
31
+ }
32
+
33
+ createWorkerEnv(
34
+ baseEnv: Record<string, string>,
35
+ workerId: number,
36
+ totalWorkers: number,
37
+ basePort?: number
38
+ ): Record<string, string> {
39
+ return {
40
+ ...baseEnv,
41
+ BM2_CLUSTER: "true",
42
+ BM2_WORKER_ID: String(workerId),
43
+ BM2_INSTANCES: String(totalWorkers),
44
+ NODE_APP_INSTANCE: String(workerId),
45
+ ...(basePort ? { PORT: String(basePort + workerId) } : {}),
46
+ };
47
+ }
48
+
49
+ buildWorkerCommand(config: ProcessDescription): string[] {
50
+ const cmd: string[] = [];
51
+
52
+ if (config.interpreter) {
53
+ cmd.push(config.interpreter);
54
+ if (config.interpreterArgs) cmd.push(...config.interpreterArgs);
55
+ } else {
56
+ const ext = config.script.split(".").pop()?.toLowerCase();
57
+ if (ext === "ts" || ext === "tsx" || ext === "js" || ext === "jsx" || ext === "mjs") {
58
+ cmd.push("bun", "run");
59
+ } else if (ext === "py") {
60
+ cmd.push("python3");
61
+ } else {
62
+ cmd.push("bun", "run");
63
+ }
64
+ }
65
+
66
+ if (config.nodeArgs?.length) {
67
+ cmd.push(...config.nodeArgs);
68
+ }
69
+
70
+ cmd.push(config.script);
71
+ if (config.args?.length) cmd.push(...config.args);
72
+
73
+ return cmd;
74
+ }
75
+
76
+ spawnWorker(
77
+ config: ProcessDescription,
78
+ workerId: number,
79
+ totalWorkers: number,
80
+ logStreams: { stdout: "pipe" | "inherit"; stderr: "pipe" | "inherit" }
81
+ ): Subprocess {
82
+ const cmd = this.buildWorkerCommand(config);
83
+ const env = this.createWorkerEnv(
84
+ { ...process.env as Record<string, string>, ...config.env },
85
+ workerId,
86
+ totalWorkers,
87
+ config.port
88
+ );
89
+
90
+ const proc = Bun.spawn(cmd, {
91
+ cwd: config.cwd || process.cwd(),
92
+ env,
93
+ stdout: logStreams.stdout,
94
+ stderr: logStreams.stderr,
95
+ stdin: "ignore",
96
+ });
97
+
98
+ if (!this.workers.has(config.id)) {
99
+ this.workers.set(config.id, new Map());
100
+ }
101
+ this.workers.get(config.id)!.set(workerId, proc);
102
+
103
+ return proc;
104
+ }
105
+
106
+ getWorkers(processId: number): Map<number, Subprocess> | undefined {
107
+ return this.workers.get(processId);
108
+ }
109
+
110
+ removeWorker(processId: number, workerId: number) {
111
+ this.workers.get(processId)?.delete(workerId);
112
+ }
113
+
114
+ removeAllWorkers(processId: number) {
115
+ this.workers.delete(processId);
116
+ }
117
+ }
@@ -0,0 +1,44 @@
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 { homedir } from "os";
18
+ import { join } from "path";
19
+
20
+ export const APP_NAME = "bm2";
21
+ export const VERSION = "1.0.0";
22
+
23
+ export const BM2_HOME = join(homedir(), ".bm2");
24
+ export const DAEMON_SOCKET = join(BM2_HOME, "daemon.sock");
25
+ export const DAEMON_PID_FILE = join(BM2_HOME, "daemon.pid");
26
+ export const LOG_DIR = join(BM2_HOME, "logs");
27
+ export const PID_DIR = join(BM2_HOME, "pids");
28
+ export const DUMP_FILE = join(BM2_HOME, "dump.json");
29
+ export const METRICS_DIR = join(BM2_HOME, "metrics");
30
+ export const MODULE_DIR = join(BM2_HOME, "modules");
31
+ export const CONFIG_FILE = join(BM2_HOME, "config.json");
32
+ export const DASHBOARD_PORT = 9615;
33
+ export const METRICS_PORT = 9616;
34
+
35
+ export const ALL_DIRS = [BM2_HOME, LOG_DIR, PID_DIR, METRICS_DIR, MODULE_DIR];
36
+
37
+ export const DEFAULT_KILL_TIMEOUT = 5000;
38
+ export const DEFAULT_MIN_UPTIME = 1000;
39
+ export const DEFAULT_MAX_RESTARTS = 16;
40
+ export const DEFAULT_RESTART_DELAY = 0;
41
+ export const DEFAULT_LOG_MAX_SIZE = 10 * 1024 * 1024; // 10MB
42
+ export const DEFAULT_LOG_RETAIN = 5;
43
+ export const MONITOR_INTERVAL = 1000;
44
+ export const HEALTH_CHECK_INTERVAL = 30000;
@@ -0,0 +1,74 @@
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 { parseCron } from "./utils";
17
+
18
+ export class CronManager {
19
+ private jobs: Map<number, {
20
+ expression: string;
21
+ timer: ReturnType<typeof setTimeout>;
22
+ }> = new Map();
23
+
24
+ schedule(processId: number, expression: string, callback: () => void) {
25
+ this.cancel(processId);
26
+
27
+ const scheduleNext = () => {
28
+ try {
29
+ const cron = parseCron(expression);
30
+ const nextDate = cron.next();
31
+ const delay = nextDate.getTime() - Date.now();
32
+
33
+ if (delay < 0) {
34
+ // Schedule for next minute at least
35
+ setTimeout(scheduleNext, 60000);
36
+ return;
37
+ }
38
+
39
+ const timer = setTimeout(() => {
40
+ callback();
41
+ scheduleNext(); // Schedule the next occurrence
42
+ }, delay);
43
+
44
+ this.jobs.set(processId, { expression, timer });
45
+ } catch (err) {
46
+ console.error(`[bm2] Cron schedule error for process ${processId}:`, err);
47
+ }
48
+ };
49
+
50
+ scheduleNext();
51
+ }
52
+
53
+ cancel(processId: number) {
54
+ const job = this.jobs.get(processId);
55
+ if (job) {
56
+ clearTimeout(job.timer);
57
+ this.jobs.delete(processId);
58
+ }
59
+ }
60
+
61
+ cancelAll() {
62
+ for (const [id] of this.jobs) {
63
+ this.cancel(id);
64
+ }
65
+ }
66
+
67
+ listJobs() {
68
+ const result: Array<{ processId: number; expression: string }> = [];
69
+ for (const [id, job] of this.jobs) {
70
+ result.push({ processId: id, expression: job.expression });
71
+ }
72
+ return result;
73
+ }
74
+ }
package/src/daemon.ts ADDED
@@ -0,0 +1,233 @@
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 { ServerWebSocket } from "bun";
30
+
31
+ ensureDirs();
32
+
33
+ const pm = new ProcessManager();
34
+ const dashboard = new Dashboard(pm);
35
+ const moduleManager = new ModuleManager(pm);
36
+
37
+ // Clean up existing socket
38
+ if (existsSync(DAEMON_SOCKET)) {
39
+ try { unlinkSync(DAEMON_SOCKET); } catch {}
40
+ }
41
+
42
+ // Write PID file
43
+ await Bun.write(DAEMON_PID_FILE, String(process.pid));
44
+
45
+ // Load modules
46
+ await moduleManager.loadAll();
47
+
48
+ // Start metric collection
49
+ const metricsInterval = setInterval(() => {
50
+ pm.getMetrics();
51
+ }, 2000);
52
+
53
+ async function handleMessage(msg: DaemonMessage): Promise<DaemonResponse> {
54
+ try {
55
+ switch (msg.type) {
56
+ case "start": {
57
+ const states = await pm.start(msg.data);
58
+ return { type: "start", data: states, success: true, id: msg.id };
59
+ }
60
+ case "stop": {
61
+ const states = await pm.stop(msg.data.target);
62
+ return { type: "stop", data: states, success: true, id: msg.id };
63
+ }
64
+ case "restart": {
65
+ const states = await pm.restart(msg.data.target);
66
+ return { type: "restart", data: states, success: true, id: msg.id };
67
+ }
68
+ case "reload": {
69
+ const states = await pm.reload(msg.data.target);
70
+ return { type: "reload", data: states, success: true, id: msg.id };
71
+ }
72
+ case "delete": {
73
+ const states = await pm.del(msg.data.target);
74
+ return { type: "delete", data: states, success: true, id: msg.id };
75
+ }
76
+ case "scale": {
77
+ const states = await pm.scale(msg.data.target, msg.data.count);
78
+ return { type: "scale", data: states, success: true, id: msg.id };
79
+ }
80
+ case "stopAll": {
81
+ const states = await pm.stopAll();
82
+ return { type: "stopAll", data: states, success: true, id: msg.id };
83
+ }
84
+ case "restartAll": {
85
+ const states = await pm.restartAll();
86
+ return { type: "restartAll", data: states, success: true, id: msg.id };
87
+ }
88
+ case "reloadAll": {
89
+ const states = await pm.reloadAll();
90
+ return { type: "reloadAll", data: states, success: true, id: msg.id };
91
+ }
92
+ case "deleteAll": {
93
+ const states = await pm.deleteAll();
94
+ return { type: "deleteAll", data: states, success: true, id: msg.id };
95
+ }
96
+ case "list": {
97
+ return { type: "list", data: pm.list(), success: true, id: msg.id };
98
+ }
99
+ case "describe": {
100
+ return { type: "describe", data: pm.describe(msg.data.target), success: true, id: msg.id };
101
+ }
102
+ case "logs": {
103
+ const logs = await pm.getLogs(msg.data.target, msg.data.lines);
104
+ return { type: "logs", data: logs, success: true, id: msg.id };
105
+ }
106
+ case "flush": {
107
+ await pm.flushLogs(msg.data?.target);
108
+ return { type: "flush", success: true, id: msg.id };
109
+ }
110
+ case "save": {
111
+ await pm.save();
112
+ return { type: "save", success: true, id: msg.id };
113
+ }
114
+ case "resurrect": {
115
+ const states = await pm.resurrect();
116
+ return { type: "resurrect", data: states, success: true, id: msg.id };
117
+ }
118
+ case "ecosystem": {
119
+ const states = await pm.startEcosystem(msg.data);
120
+ return { type: "ecosystem", data: states, success: true, id: msg.id };
121
+ }
122
+ case "signal": {
123
+ await pm.sendSignal(msg.data.target, msg.data.signal);
124
+ return { type: "signal", success: true, id: msg.id };
125
+ }
126
+ case "reset": {
127
+ const states = await pm.reset(msg.data.target);
128
+ return { type: "reset", data: states, success: true, id: msg.id };
129
+ }
130
+ case "metrics": {
131
+ const metrics = await pm.getMetrics();
132
+ return { type: "metrics", data: metrics, success: true, id: msg.id };
133
+ }
134
+ case "metricsHistory": {
135
+ const history = pm.getMetricsHistory(msg.data?.seconds || 300);
136
+ return { type: "metricsHistory", data: history, success: true, id: msg.id };
137
+ }
138
+ case "prometheus": {
139
+ const prom = pm.getPrometheusMetrics();
140
+ return { type: "prometheus", data: prom, success: true, id: msg.id };
141
+ }
142
+ case "dashboard": {
143
+ const port = msg.data?.port || DASHBOARD_PORT;
144
+ const metricsPort = msg.data?.metricsPort || METRICS_PORT;
145
+ dashboard.start(port, metricsPort);
146
+ return { type: "dashboard", data: { port, metricsPort }, success: true, id: msg.id };
147
+ }
148
+ case "dashboardStop": {
149
+ dashboard.stop();
150
+ return { type: "dashboardStop", success: true, id: msg.id };
151
+ }
152
+ case "moduleInstall": {
153
+ const path = await moduleManager.install(msg.data.module);
154
+ return { type: "moduleInstall", data: { path }, success: true, id: msg.id };
155
+ }
156
+ case "moduleUninstall": {
157
+ await moduleManager.uninstall(msg.data.module);
158
+ return { type: "moduleUninstall", success: true, id: msg.id };
159
+ }
160
+ case "moduleList": {
161
+ return { type: "moduleList", data: moduleManager.list(), success: true, id: msg.id };
162
+ }
163
+ case "ping": {
164
+ return {
165
+ type: "pong",
166
+ data: { pid: process.pid, uptime: process.uptime() },
167
+ success: true,
168
+ id: msg.id,
169
+ };
170
+ }
171
+ case "kill": {
172
+ await pm.stopAll();
173
+ dashboard.stop();
174
+ clearInterval(metricsInterval);
175
+ setTimeout(() => process.exit(0), 200);
176
+ return { type: "kill", success: true, id: msg.id };
177
+ }
178
+ default:
179
+ return { type: "error", error: `Unknown command: ${msg.type}`, success: false, id: msg.id };
180
+ }
181
+ } catch (err: any) {
182
+ return { type: "error", error: err.message, success: false, id: msg.id };
183
+ }
184
+ }
185
+
186
+ // Unix socket server
187
+ const server = Bun.serve({
188
+ unix: DAEMON_SOCKET,
189
+ fetch(req, server) {
190
+ if (server.upgrade(req)) return;
191
+ return new Response("bm2 daemon");
192
+ },
193
+ websocket: {
194
+ async message(ws: ServerWebSocket<unknown>, message) {
195
+ try {
196
+ const msg: DaemonMessage = JSON.parse(String(message));
197
+ const response = await handleMessage(msg);
198
+ ws.send(JSON.stringify(response));
199
+ } catch (err: any) {
200
+ ws.send(JSON.stringify({ type: "error", error: err.message, success: false }));
201
+ }
202
+ },
203
+ open(ws) {},
204
+ close(ws) {},
205
+ },
206
+ });
207
+
208
+ // Signal handlers
209
+ const shutdown = async () => {
210
+ console.log("\n[bm2] Shutting down daemon...");
211
+ await pm.stopAll();
212
+ dashboard.stop();
213
+ clearInterval(metricsInterval);
214
+ try { unlinkSync(DAEMON_SOCKET); } catch {}
215
+ try { unlinkSync(DAEMON_PID_FILE); } catch {}
216
+ process.exit(0);
217
+ };
218
+
219
+ process.on("SIGTERM", shutdown);
220
+ process.on("SIGINT", shutdown);
221
+ process.on("SIGHUP", shutdown);
222
+
223
+ // Handle uncaught errors to keep daemon alive
224
+ process.on("uncaughtException", (err) => {
225
+ console.error("[bm2] Uncaught exception:", err);
226
+ });
227
+
228
+ process.on("unhandledRejection", (err) => {
229
+ console.error("[bm2] Unhandled rejection:", err);
230
+ });
231
+
232
+ console.log(`[bm2] Daemon running (PID: ${process.pid})`);
233
+ console.log(`[bm2] Socket: ${DAEMON_SOCKET}`);