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.
@@ -0,0 +1,403 @@
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 {
17
+ ProcessDescription,
18
+ ProcessState,
19
+ StartOptions,
20
+ EcosystemConfig,
21
+ MetricSnapshot,
22
+ } from "./types";
23
+ import { ProcessContainer } from "./process-container";
24
+ import { LogManager } from "./log-manager";
25
+ import { ClusterManager } from "./cluster-manager";
26
+ import { HealthChecker } from "./health-checker";
27
+ import { CronManager } from "./cron-manager";
28
+ import { Monitor } from "./monitor";
29
+ import { GracefulReload } from "./graceful-reload";
30
+ import { parseMemory, DUMP_FILE } from "./utils";
31
+ import {
32
+ DEFAULT_KILL_TIMEOUT,
33
+ DEFAULT_MAX_RESTARTS,
34
+ DEFAULT_MIN_UPTIME,
35
+ DEFAULT_RESTART_DELAY,
36
+ DEFAULT_LOG_MAX_SIZE,
37
+ DEFAULT_LOG_RETAIN,
38
+ } from "./constants";
39
+
40
+ export class ProcessManager {
41
+ private processes: Map<number, ProcessContainer> = new Map();
42
+ private nextId: number = 0;
43
+ public logManager: LogManager;
44
+ public clusterManager: ClusterManager;
45
+ public healthChecker: HealthChecker;
46
+ public cronManager: CronManager;
47
+ public monitor: Monitor;
48
+ public gracefulReload: GracefulReload;
49
+
50
+ constructor() {
51
+ this.logManager = new LogManager();
52
+ this.clusterManager = new ClusterManager();
53
+ this.healthChecker = new HealthChecker();
54
+ this.cronManager = new CronManager();
55
+ this.monitor = new Monitor();
56
+ this.gracefulReload = new GracefulReload();
57
+ }
58
+
59
+ async start(options: StartOptions): Promise<ProcessState[]> {
60
+ const resolvedInstances = this.clusterManager.resolveInstances(options.instances);
61
+ const isCluster = options.execMode === "cluster" || resolvedInstances > 1;
62
+ const states: ProcessState[] = [];
63
+
64
+ if (isCluster) {
65
+ // In cluster mode, each instance is a separate container
66
+ for (let i = 0; i < resolvedInstances; i++) {
67
+ const id = this.nextId++;
68
+ const baseName =
69
+ options.name ||
70
+ options.script.split("/").pop()?.replace(/\.\w+$/, "") ||
71
+ `app-${id}`;
72
+ const name = resolvedInstances > 1 ? `${baseName}-${i}` : baseName;
73
+
74
+ const config = this.buildConfig(id, name, options, resolvedInstances, i);
75
+ const container = new ProcessContainer(
76
+ id, config, this.logManager, this.clusterManager,
77
+ this.healthChecker, this.cronManager
78
+ );
79
+
80
+ this.processes.set(id, container);
81
+ await container.start();
82
+ states.push(container.getState());
83
+ }
84
+ } else {
85
+ const id = this.nextId++;
86
+ const name =
87
+ options.name ||
88
+ options.script.split("/").pop()?.replace(/\.\w+$/, "") ||
89
+ `app-${id}`;
90
+
91
+ const config = this.buildConfig(id, name, options, 1, 0);
92
+ const container = new ProcessContainer(
93
+ id, config, this.logManager, this.clusterManager,
94
+ this.healthChecker, this.cronManager
95
+ );
96
+
97
+ this.processes.set(id, container);
98
+ await container.start();
99
+ states.push(container.getState());
100
+ }
101
+
102
+ return states;
103
+ }
104
+
105
+ private buildConfig(
106
+ id: number,
107
+ name: string,
108
+ options: StartOptions,
109
+ instances: number,
110
+ workerIndex: number
111
+ ): ProcessDescription {
112
+ return {
113
+ id,
114
+ name,
115
+ script: options.script,
116
+ args: options.args || [],
117
+ cwd: options.cwd || process.cwd(),
118
+ env: {
119
+ ...options.env,
120
+ ...(instances > 1
121
+ ? {
122
+ NODE_APP_INSTANCE: String(workerIndex),
123
+ BM2_INSTANCE_ID: String(workerIndex),
124
+ }
125
+ : {}),
126
+ },
127
+ instances,
128
+ execMode: instances > 1 ? "cluster" : (options.execMode || "fork"),
129
+ autorestart: options.autorestart !== false,
130
+ maxRestarts: options.maxRestarts ?? DEFAULT_MAX_RESTARTS,
131
+ minUptime: options.minUptime ?? DEFAULT_MIN_UPTIME,
132
+ maxMemoryRestart: options.maxMemoryRestart
133
+ ? parseMemory(options.maxMemoryRestart)
134
+ : undefined,
135
+ watch: Array.isArray(options.watch) ? true : (options.watch ?? false),
136
+ watchPaths: Array.isArray(options.watch) ? options.watch : undefined,
137
+ ignoreWatch: options.ignoreWatch || ["node_modules", ".git", ".bm2"],
138
+ cronRestart: options.cron,
139
+ interpreter: options.interpreter,
140
+ interpreterArgs: options.interpreterArgs,
141
+ mergeLogs: options.mergeLogs ?? false,
142
+ logDateFormat: options.logDateFormat,
143
+ errorFile: options.errorFile,
144
+ outFile: options.outFile,
145
+ killTimeout: options.killTimeout ?? DEFAULT_KILL_TIMEOUT,
146
+ restartDelay: options.restartDelay ?? DEFAULT_RESTART_DELAY,
147
+ port: options.port,
148
+ healthCheckUrl: options.healthCheckUrl,
149
+ healthCheckInterval: options.healthCheckInterval,
150
+ healthCheckTimeout: options.healthCheckTimeout,
151
+ healthCheckMaxFails: options.healthCheckMaxFails,
152
+ logMaxSize: options.logMaxSize ? parseMemory(options.logMaxSize) : DEFAULT_LOG_MAX_SIZE,
153
+ logRetain: options.logRetain ?? DEFAULT_LOG_RETAIN,
154
+ logCompress: options.logCompress,
155
+ waitReady: options.waitReady,
156
+ listenTimeout: options.listenTimeout,
157
+ namespace: options.namespace,
158
+ nodeArgs: options.nodeArgs,
159
+ sourceMapSupport: options.sourceMapSupport,
160
+ treekill: true,
161
+ };
162
+ }
163
+
164
+ async stop(target: string | number): Promise<ProcessState[]> {
165
+ const containers = this.resolveTarget(target);
166
+ const states: ProcessState[] = [];
167
+ for (const c of containers) {
168
+ await c.stop();
169
+ states.push(c.getState());
170
+ }
171
+ return states;
172
+ }
173
+
174
+ async restart(target: string | number): Promise<ProcessState[]> {
175
+ const containers = this.resolveTarget(target);
176
+ const states: ProcessState[] = [];
177
+ for (const c of containers) {
178
+ await c.restart();
179
+ states.push(c.getState());
180
+ }
181
+ return states;
182
+ }
183
+
184
+ async reload(target: string | number): Promise<ProcessState[]> {
185
+ const containers = this.resolveTarget(target);
186
+ // Use graceful reload for zero downtime
187
+ await this.gracefulReload.reload(containers);
188
+ return containers.map((c) => c.getState());
189
+ }
190
+
191
+ async del(target: string | number): Promise<ProcessState[]> {
192
+ const containers = this.resolveTarget(target);
193
+ const states: ProcessState[] = [];
194
+ for (const c of containers) {
195
+ await c.stop(true);
196
+ states.push(c.getState());
197
+ this.processes.delete(c.id);
198
+ }
199
+ return states;
200
+ }
201
+
202
+ async stopAll(): Promise<ProcessState[]> {
203
+ const states: ProcessState[] = [];
204
+ for (const c of this.processes.values()) {
205
+ await c.stop();
206
+ states.push(c.getState());
207
+ }
208
+ return states;
209
+ }
210
+
211
+ async restartAll(): Promise<ProcessState[]> {
212
+ const states: ProcessState[] = [];
213
+ for (const c of this.processes.values()) {
214
+ await c.restart();
215
+ states.push(c.getState());
216
+ }
217
+ return states;
218
+ }
219
+
220
+ async reloadAll(): Promise<ProcessState[]> {
221
+ const containers = Array.from(this.processes.values());
222
+ await this.gracefulReload.reload(containers);
223
+ return containers.map((c) => c.getState());
224
+ }
225
+
226
+ async deleteAll(): Promise<ProcessState[]> {
227
+ const states: ProcessState[] = [];
228
+ for (const c of this.processes.values()) {
229
+ await c.stop(true);
230
+ states.push(c.getState());
231
+ }
232
+ this.processes.clear();
233
+ this.nextId = 0;
234
+ return states;
235
+ }
236
+
237
+ async scale(target: string | number, count: number): Promise<ProcessState[]> {
238
+ const containers = this.resolveTarget(target);
239
+ if (containers.length === 0) return [];
240
+
241
+ const first = containers[0]!;
242
+ const baseName = first.name.replace(/-\d+$/, "");
243
+ const currentCount = containers.length;
244
+
245
+ if (count > currentCount) {
246
+ // Scale up
247
+ const toAdd = count - currentCount;
248
+ const baseConfig = first.config;
249
+ const states: ProcessState[] = [];
250
+
251
+ for (let i = 0; i < toAdd; i++) {
252
+ const result = await this.start({
253
+ name: `${baseName}-${currentCount + i}`,
254
+ script: baseConfig.script,
255
+ args: baseConfig.args,
256
+ cwd: baseConfig.cwd,
257
+ env: baseConfig.env,
258
+ execMode: baseConfig.execMode,
259
+ autorestart: baseConfig.autorestart,
260
+ maxRestarts: baseConfig.maxRestarts,
261
+ watch: baseConfig.watch,
262
+ port: baseConfig.port,
263
+ });
264
+ states.push(...result);
265
+ }
266
+
267
+ return [...containers.map((c) => c.getState()), ...states];
268
+ } else if (count < currentCount) {
269
+ // Scale down
270
+ const toRemove = containers.slice(count);
271
+ for (const c of toRemove) {
272
+ await c.stop(true);
273
+ this.processes.delete(c.id);
274
+ }
275
+ return containers.slice(0, count).map((c) => c.getState());
276
+ }
277
+
278
+ return containers.map((c) => c.getState());
279
+ }
280
+
281
+ list(): ProcessState[] {
282
+ return Array.from(this.processes.values()).map((p) => p.getState());
283
+ }
284
+
285
+ describe(target: string | number): ProcessState[] {
286
+ return this.resolveTarget(target).map((p) => p.getState());
287
+ }
288
+
289
+ async getLogs(target: string | number, lines: number = 20) {
290
+ const containers = this.resolveTarget(target);
291
+ const results: Array<{ name: string; id: number; out: string; err: string }> = [];
292
+ for (const c of containers) {
293
+ const logs = await this.logManager.readLogs(
294
+ c.name, c.id, lines, c.config.outFile, c.config.errorFile
295
+ );
296
+ results.push({ name: c.name, id: c.id, ...logs });
297
+ }
298
+ return results;
299
+ }
300
+
301
+ async flushLogs(target?: string | number) {
302
+ const containers = target
303
+ ? this.resolveTarget(target)
304
+ : Array.from(this.processes.values());
305
+ for (const c of containers) {
306
+ await this.logManager.flush(c.name, c.id, c.config.outFile, c.config.errorFile);
307
+ }
308
+ }
309
+
310
+ async save(): Promise<void> {
311
+ const data = Array.from(this.processes.values()).map((p) => ({
312
+ config: p.config,
313
+ restartCount: p.restartCount,
314
+ }));
315
+ await Bun.write(DUMP_FILE, JSON.stringify(data, null, 2));
316
+ }
317
+
318
+ async resurrect(): Promise<ProcessState[]> {
319
+ try {
320
+ const file = Bun.file(DUMP_FILE);
321
+ if (!(await file.exists())) return [];
322
+ const data = await file.json();
323
+ const states: ProcessState[] = [];
324
+
325
+ for (const item of data) {
326
+ const result = await this.start({
327
+ name: item.config.name,
328
+ script: item.config.script,
329
+ args: item.config.args,
330
+ cwd: item.config.cwd,
331
+ env: item.config.env,
332
+ autorestart: item.config.autorestart,
333
+ maxRestarts: item.config.maxRestarts,
334
+ watch: item.config.watch,
335
+ instances: 1,
336
+ execMode: item.config.execMode,
337
+ port: item.config.port,
338
+ healthCheckUrl: item.config.healthCheckUrl,
339
+ });
340
+ states.push(...result);
341
+ }
342
+ return states;
343
+ } catch {
344
+ return [];
345
+ }
346
+ }
347
+
348
+ async startEcosystem(config: EcosystemConfig): Promise<ProcessState[]> {
349
+ const states: ProcessState[] = [];
350
+ for (const app of config.apps) {
351
+ const result = await this.start(app);
352
+ states.push(...result);
353
+ }
354
+ return states;
355
+ }
356
+
357
+ async sendSignal(target: string | number, signal: string): Promise<void> {
358
+ for (const c of this.resolveTarget(target)) {
359
+ await c.sendSignal(signal);
360
+ }
361
+ }
362
+
363
+ async getMetrics(): Promise<MetricSnapshot> {
364
+ return this.monitor.takeSnapshot(this.list());
365
+ }
366
+
367
+ getPrometheusMetrics(): string {
368
+ return this.monitor.generatePrometheusMetrics(this.list());
369
+ }
370
+
371
+ getMetricsHistory(seconds: number = 300): MetricSnapshot[] {
372
+ return this.monitor.getHistory(seconds);
373
+ }
374
+
375
+ async reset(target: string | number): Promise<ProcessState[]> {
376
+ const containers = this.resolveTarget(target);
377
+ for (const c of containers) {
378
+ c.restartCount = 0;
379
+ c.unstableRestarts = 0;
380
+ }
381
+ return containers.map((c) => c.getState());
382
+ }
383
+
384
+ private resolveTarget(target: string | number): ProcessContainer[] {
385
+ if (target === "all") {
386
+ return Array.from(this.processes.values());
387
+ }
388
+
389
+ if (typeof target === "number" || /^\d+$/.test(String(target))) {
390
+ const id = typeof target === "number" ? target : parseInt(target);
391
+ const proc = this.processes.get(id);
392
+ return proc ? [proc] : [];
393
+ }
394
+
395
+ // Match by name or namespace
396
+ return Array.from(this.processes.values()).filter(
397
+ (p) =>
398
+ p.name === target ||
399
+ p.name.startsWith(`${target}-`) ||
400
+ p.config.namespace === target
401
+ );
402
+ }
403
+ }
package/src/startup.ts ADDED
@@ -0,0 +1,158 @@
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 { join } from "path";
18
+
19
+ export class StartupManager {
20
+ async generate(platform?: string): Promise<string> {
21
+ const os = platform || process.platform;
22
+ const bunPath = Bun.which("bun") || "/usr/local/bin/bun";
23
+ const bm2Path = join(import.meta.dir, "index.ts");
24
+ const daemonPath = join(import.meta.dir, "daemon.ts");
25
+
26
+ switch (os) {
27
+ case "linux":
28
+ return this.generateSystemd(bunPath, bm2Path, daemonPath);
29
+ case "darwin":
30
+ return this.generateLaunchd(bunPath, bm2Path, daemonPath);
31
+ default:
32
+ throw new Error(`Unsupported platform: ${os}`);
33
+ }
34
+ }
35
+
36
+ private generateSystemd(bunPath: string, bm2Path: string, daemonPath: string): string {
37
+ const unit = `[Unit]
38
+ Description=BM2 Process Manager
39
+ Documentation=https://github.com/bm2
40
+ After=network.target
41
+
42
+ [Service]
43
+ Type=forking
44
+ User=${process.env.USER || "root"}
45
+ LimitNOFILE=infinity
46
+ LimitNPROC=infinity
47
+ LimitCORE=infinity
48
+ Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${join(bunPath, "..")}
49
+ Environment=BM2_HOME=${join(process.env.HOME || "/root", ".bm2")}
50
+ PIDFile=${join(process.env.HOME || "/root", ".bm2", "daemon.pid")}
51
+ Restart=on-failure
52
+
53
+ ExecStart=${bunPath} run ${daemonPath}
54
+ ExecReload=${bunPath} run ${bm2Path} reload all
55
+ ExecStop=${bunPath} run ${bm2Path} kill
56
+
57
+ [Install]
58
+ WantedBy=multi-user.target`;
59
+
60
+ const servicePath = "/etc/systemd/system/bm2.service";
61
+ return `# BM2 Systemd Service
62
+ # Save to: ${servicePath}
63
+ # Then run:
64
+ # sudo systemctl daemon-reload
65
+ # sudo systemctl enable bm2
66
+ # sudo systemctl start bm2
67
+
68
+ ${unit}`;
69
+ }
70
+
71
+ private generateLaunchd(bunPath: string, bm2Path: string, daemonPath: string): string {
72
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
73
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
74
+ <plist version="1.0">
75
+ <dict>
76
+ <key>Label</key>
77
+ <string>com.bm2.daemon</string>
78
+ <key>ProgramArguments</key>
79
+ <array>
80
+ <string>${bunPath}</string>
81
+ <string>run</string>
82
+ <string>${daemonPath}</string>
83
+ </array>
84
+ <key>RunAtLoad</key>
85
+ <true/>
86
+ <key>KeepAlive</key>
87
+ <true/>
88
+ <key>StandardOutPath</key>
89
+ <string>${join(process.env.HOME || "/Users/user", ".bm2", "logs", "daemon-out.log")}</string>
90
+ <key>StandardErrorPath</key>
91
+ <string>${join(process.env.HOME || "/Users/user", ".bm2", "logs", "daemon-error.log")}</string>
92
+ <key>EnvironmentVariables</key>
93
+ <dict>
94
+ <key>PATH</key>
95
+ <string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
96
+ <key>HOME</key>
97
+ <string>${process.env.HOME}</string>
98
+ </dict>
99
+ </dict>
100
+ </plist>`;
101
+
102
+ const plistPath = `${process.env.HOME}/Library/LaunchAgents/com.bm2.daemon.plist`;
103
+ return `# BM2 LaunchAgent (macOS)
104
+ # Save to: ${plistPath}
105
+ # Then run:
106
+ # launchctl load ${plistPath}
107
+
108
+ ${plist}`;
109
+ }
110
+
111
+ async install(): Promise<string> {
112
+ const os = process.platform;
113
+ const content = await this.generate(os);
114
+
115
+ if (os === "linux") {
116
+ const servicePath = "/etc/systemd/system/bm2.service";
117
+ // Extract just the unit content
118
+ const unitContent = content.split("\n\n").slice(1).join("\n\n");
119
+ await Bun.write(servicePath, unitContent);
120
+
121
+ Bun.spawn(["sudo", "systemctl", "daemon-reload"], { stdout: "inherit" });
122
+ Bun.spawn(["sudo", "systemctl", "enable", "bm2"], { stdout: "inherit" });
123
+
124
+ return `Service installed at ${servicePath}\nRun: sudo systemctl start bm2`;
125
+ } else if (os === "darwin") {
126
+ const plistPath = `${process.env.HOME}/Library/LaunchAgents/com.bm2.daemon.plist`;
127
+ // Extract plist content
128
+ const plistStart = content.indexOf("<?xml");
129
+ const plistContent = content.substring(plistStart);
130
+ await Bun.write(plistPath, plistContent);
131
+
132
+ return `Plist installed at ${plistPath}\nRun: launchctl load ${plistPath}`;
133
+ }
134
+
135
+ return "Unsupported platform for auto-install. Manual setup required.";
136
+ }
137
+
138
+ async uninstall(): Promise<string> {
139
+ const os = process.platform;
140
+
141
+ if (os === "linux") {
142
+ Bun.spawn(["sudo", "systemctl", "stop", "bm2"], { stdout: "inherit" });
143
+ Bun.spawn(["sudo", "systemctl", "disable", "bm2"], { stdout: "inherit" });
144
+ const { unlinkSync } = require("fs");
145
+ try { unlinkSync("/etc/systemd/system/bm2.service"); } catch {}
146
+ Bun.spawn(["sudo", "systemctl", "daemon-reload"], { stdout: "inherit" });
147
+ return "BM2 service removed";
148
+ } else if (os === "darwin") {
149
+ const plistPath = `${process.env.HOME}/Library/LaunchAgents/com.bm2.daemon.plist`;
150
+ Bun.spawn(["launchctl", "unload", plistPath], { stdout: "inherit" });
151
+ const { unlinkSync } = require("fs");
152
+ try { unlinkSync(plistPath); } catch {}
153
+ return "BM2 launch agent removed";
154
+ }
155
+
156
+ return "Unsupported platform";
157
+ }
158
+ }