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,77 @@
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
+ import { BM2_HOME } from "./constants";
19
+
20
+ export class EnvManager {
21
+ private envFile = join(BM2_HOME, "env-registry.json");
22
+
23
+ async getEnvs(): Promise<Record<string, Record<string, string>>> {
24
+ try {
25
+ const file = Bun.file(this.envFile);
26
+ if (await file.exists()) return await file.json();
27
+ } catch {}
28
+ return {};
29
+ }
30
+
31
+ async setEnv(name: string, key: string, value: string): Promise<void> {
32
+ const envs = await this.getEnvs();
33
+ if (!envs[name]) envs[name] = {};
34
+ envs[name][key] = value;
35
+ await Bun.write(this.envFile, JSON.stringify(envs, null, 2));
36
+ }
37
+
38
+ async getEnv(name: string): Promise<Record<string, string>> {
39
+ const envs = await this.getEnvs();
40
+ return envs[name] || {};
41
+ }
42
+
43
+ async deleteEnv(name: string, key?: string): Promise<void> {
44
+ const envs = await this.getEnvs();
45
+ if (key) {
46
+ delete envs[name]?.[key];
47
+ } else {
48
+ delete envs[name];
49
+ }
50
+ await Bun.write(this.envFile, JSON.stringify(envs, null, 2));
51
+ }
52
+
53
+ async loadDotEnv(filePath: string): Promise<Record<string, string>> {
54
+ const file = Bun.file(filePath);
55
+ if (!(await file.exists())) return {};
56
+
57
+ const content = await file.text();
58
+ const env: Record<string, string> = {};
59
+
60
+ for (const line of content.split("\n")) {
61
+ const trimmed = line.trim();
62
+ if (!trimmed || trimmed.startsWith("#")) continue;
63
+ const eqIdx = trimmed.indexOf("=");
64
+ if (eqIdx === -1) continue;
65
+ const key = trimmed.substring(0, eqIdx).trim();
66
+ let value = trimmed.substring(eqIdx + 1).trim();
67
+ // Remove quotes
68
+ if ((value.startsWith('"') && value.endsWith('"')) ||
69
+ (value.startsWith("'") && value.endsWith("'"))) {
70
+ value = value.slice(1, -1);
71
+ }
72
+ env[key] = value;
73
+ }
74
+
75
+ return env;
76
+ }
77
+ }
@@ -0,0 +1,71 @@
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 { ProcessContainer } from "./process-container";
17
+
18
+ export class GracefulReload {
19
+ async reload(
20
+ containers: ProcessContainer[],
21
+ options: {
22
+ delay?: number;
23
+ listenTimeout?: number;
24
+ } = {}
25
+ ): Promise<void> {
26
+ const delay = options.delay || 1000;
27
+ const listenTimeout = options.listenTimeout || 3000;
28
+
29
+ for (let i = 0; i < containers.length; i++) {
30
+ const container = containers[i];
31
+ if (!container) continue;
32
+
33
+ const oldPid = container.pid;
34
+
35
+ console.log(`[bm2] Graceful reload: reloading ${container.name} (${i + 1}/${containers.length})`);
36
+
37
+ const startPromise = container.start();
38
+
39
+ if (container.config.waitReady) {
40
+ await Promise.race([
41
+ new Promise<void>((resolve) => {
42
+ const checkReady = setInterval(() => {
43
+ if (container.status === "online") {
44
+ clearInterval(checkReady);
45
+ resolve();
46
+ }
47
+ }, 100);
48
+ }),
49
+ new Promise<void>((resolve) =>
50
+ setTimeout(resolve, listenTimeout)
51
+ ),
52
+ ]);
53
+ } else {
54
+ await startPromise;
55
+ await new Promise((resolve) => setTimeout(resolve, delay));
56
+ }
57
+
58
+ if (oldPid) {
59
+ try {
60
+ process.kill(oldPid, "SIGTERM");
61
+ } catch {}
62
+ }
63
+
64
+ if (i < containers.length - 1) {
65
+ await new Promise((resolve) => setTimeout(resolve, delay));
66
+ }
67
+ }
68
+
69
+ console.log(`[bm2] Graceful reload complete`);
70
+ }
71
+ }
@@ -0,0 +1,112 @@
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 type { HealthCheckConfig } from "./types";
18
+
19
+ export class HealthChecker {
20
+ private checks: Map<number, {
21
+ config: HealthCheckConfig;
22
+ timer: ReturnType<typeof setInterval>;
23
+ consecutiveFails: number;
24
+ lastStatus: "healthy" | "unhealthy" | "unknown";
25
+ lastCheck: number;
26
+ lastResponseTime: number;
27
+ }> = new Map();
28
+
29
+ startCheck(
30
+ processId: number,
31
+ config: HealthCheckConfig,
32
+ onUnhealthy: (processId: number, reason: string) => void
33
+ ) {
34
+ this.stopCheck(processId);
35
+
36
+ const state: {
37
+ config: HealthCheckConfig;
38
+ timer: ReturnType<typeof setInterval>;
39
+ consecutiveFails: number;
40
+ lastStatus: "healthy" | "unhealthy" | "unknown";
41
+ lastCheck: number;
42
+ lastResponseTime: number;
43
+ } = {
44
+ config,
45
+ consecutiveFails: 0,
46
+ lastStatus: "unknown",
47
+ lastCheck: 0,
48
+ lastResponseTime: 0,
49
+ timer: setInterval(async () => {
50
+ const start = Date.now();
51
+ try {
52
+ const controller = new AbortController();
53
+ const timeout = setTimeout(() => controller.abort(), config.timeout);
54
+
55
+ const response = await fetch(config.url, {
56
+ signal: controller.signal,
57
+ method: "GET",
58
+ });
59
+
60
+ clearTimeout(timeout);
61
+ state.lastResponseTime = Date.now() - start;
62
+ state.lastCheck = Date.now();
63
+
64
+ if (response.ok) {
65
+ state.consecutiveFails = 0;
66
+ state.lastStatus = "healthy";
67
+ } else {
68
+ state.consecutiveFails++;
69
+ state.lastStatus = "unhealthy";
70
+ }
71
+ } catch {
72
+ state.consecutiveFails++;
73
+ state.lastStatus = "unhealthy";
74
+ state.lastResponseTime = Date.now() - start;
75
+ state.lastCheck = Date.now();
76
+ }
77
+
78
+ if (state.consecutiveFails >= config.maxFails) {
79
+ onUnhealthy(processId, `Health check failed ${state.consecutiveFails} times consecutively`);
80
+ state.consecutiveFails = 0;
81
+ }
82
+ }, config.interval),
83
+ };
84
+
85
+ this.checks.set(processId, state);
86
+ }
87
+
88
+ stopCheck(processId: number) {
89
+ const check = this.checks.get(processId);
90
+ if (check) {
91
+ clearInterval(check.timer);
92
+ this.checks.delete(processId);
93
+ }
94
+ }
95
+
96
+ getStatus(processId: number) {
97
+ const check = this.checks.get(processId);
98
+ if (!check) return null;
99
+ return {
100
+ status: check.lastStatus,
101
+ lastCheck: check.lastCheck,
102
+ lastResponseTime: check.lastResponseTime,
103
+ consecutiveFails: check.consecutiveFails,
104
+ };
105
+ }
106
+
107
+ stopAll() {
108
+ for (const [id] of this.checks) {
109
+ this.stopCheck(id);
110
+ }
111
+ }
112
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
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
+ */
@@ -0,0 +1,201 @@
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, dirname } from "path";
18
+ import { existsSync, readdirSync, unlinkSync, renameSync, statSync } from "fs";
19
+ import { LOG_DIR, DEFAULT_LOG_MAX_SIZE, DEFAULT_LOG_RETAIN } from "./constants";
20
+ import type { LogRotateOptions } from "./types";
21
+
22
+ export class LogManager {
23
+ private writeBuffers: Map<string, string[]> = new Map();
24
+ private flushTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
25
+
26
+ getLogPaths(name: string, id: number, customOut?: string, customErr?: string) {
27
+ return {
28
+ outFile: customOut || join(LOG_DIR, `${name}-${id}-out.log`),
29
+ errFile: customErr || join(LOG_DIR, `${name}-${id}-error.log`),
30
+ };
31
+ }
32
+
33
+ async appendLog(filePath: string, data: string | Uint8Array) {
34
+ const text = typeof data === "string" ? data : new TextDecoder().decode(data);
35
+
36
+ // Buffer writes for performance
37
+ if (!this.writeBuffers.has(filePath)) {
38
+ this.writeBuffers.set(filePath, []);
39
+ }
40
+ this.writeBuffers.get(filePath)!.push(text);
41
+
42
+ // Debounced flush
43
+ if (!this.flushTimers.has(filePath)) {
44
+ this.flushTimers.set(filePath, setTimeout(() => {
45
+ this.flushBuffer(filePath);
46
+ }, 100));
47
+ }
48
+ }
49
+
50
+ private async flushBuffer(filePath: string) {
51
+ const buffer = this.writeBuffers.get(filePath);
52
+ if (!buffer || buffer.length === 0) return;
53
+
54
+ const content = buffer.join("");
55
+ this.writeBuffers.set(filePath, []);
56
+ this.flushTimers.delete(filePath);
57
+
58
+ try {
59
+ const file = Bun.file(filePath);
60
+ const existing = (await file.exists()) ? await file.text() : "";
61
+ await Bun.write(filePath, existing + content);
62
+ } catch (err) {
63
+ // If file too large, log the error
64
+ console.error(`[bm2] Failed to write log: ${filePath}`, err);
65
+ }
66
+ }
67
+
68
+ async forceFlush() {
69
+ for (const [filePath] of this.writeBuffers) {
70
+ await this.flushBuffer(filePath);
71
+ }
72
+ }
73
+
74
+ async readLogs(
75
+ name: string,
76
+ id: number,
77
+ lines: number = 20,
78
+ customOut?: string,
79
+ customErr?: string
80
+ ): Promise<{ out: string; err: string }> {
81
+ const paths = this.getLogPaths(name, id, customOut, customErr);
82
+ let out = "";
83
+ let err = "";
84
+
85
+ try {
86
+ const outFile = Bun.file(paths.outFile);
87
+ if (await outFile.exists()) {
88
+ const text = await outFile.text();
89
+ out = text.split("\n").slice(-lines).join("\n");
90
+ }
91
+ } catch {}
92
+
93
+ try {
94
+ const errFile = Bun.file(paths.errFile);
95
+ if (await errFile.exists()) {
96
+ const text = await errFile.text();
97
+ err = text.split("\n").slice(-lines).join("\n");
98
+ }
99
+ } catch {}
100
+
101
+ return { out, err };
102
+ }
103
+
104
+ async tailLog(
105
+ filePath: string,
106
+ callback: (line: string) => void,
107
+ signal?: AbortSignal
108
+ ): Promise<void> {
109
+ let lastSize = 0;
110
+ const file = Bun.file(filePath);
111
+ if (await file.exists()) {
112
+ lastSize = file.size;
113
+ }
114
+
115
+ const interval = setInterval(async () => {
116
+ if (signal?.aborted) {
117
+ clearInterval(interval);
118
+ return;
119
+ }
120
+ try {
121
+ const f = Bun.file(filePath);
122
+ if (!(await f.exists())) return;
123
+ const currentSize = f.size;
124
+ if (currentSize > lastSize) {
125
+ const text = await f.text();
126
+ const newContent = text.substring(lastSize);
127
+ lastSize = currentSize;
128
+ for (const line of newContent.split("\n").filter(Boolean)) {
129
+ callback(line);
130
+ }
131
+ }
132
+ } catch {}
133
+ }, 500);
134
+ }
135
+
136
+ async rotate(filePath: string, options: LogRotateOptions): Promise<void> {
137
+ try {
138
+ const file = Bun.file(filePath);
139
+ if (!(await file.exists())) return;
140
+
141
+ const stat = statSync(filePath);
142
+ if (stat.size < options.maxSize) return;
143
+
144
+ // Rotate files
145
+ for (let i = options.retain - 1; i >= 1; i--) {
146
+ const src = i === 1 ? filePath : `${filePath}.${i - 1}`;
147
+ const dst = `${filePath}.${i}`;
148
+ if (existsSync(src)) {
149
+ renameSync(src, dst);
150
+
151
+ if (options.compress && i > 0) {
152
+ // Compress rotated file using Bun's gzip
153
+ try {
154
+ const content = await Bun.file(dst).arrayBuffer();
155
+ const compressed = Bun.gzipSync(new Uint8Array(content));
156
+ await Bun.write(`${dst}.gz`, compressed);
157
+ unlinkSync(dst);
158
+ } catch {}
159
+ }
160
+ }
161
+ }
162
+
163
+ // Clean excess rotated files
164
+ const dir = dirname(filePath);
165
+ const baseName = filePath.split("/").pop()!;
166
+ try {
167
+ const files = readdirSync(dir);
168
+ const rotated = files
169
+ .filter((f) => f.startsWith(baseName + "."))
170
+ .sort()
171
+ .reverse();
172
+ for (let i = options.retain; i < rotated.length; i++) {
173
+ unlinkSync(join(dir, rotated[i]!));
174
+ }
175
+ } catch {}
176
+
177
+ // Truncate original
178
+ await Bun.write(filePath, "");
179
+ } catch (err) {
180
+ console.error(`[bm2] Log rotation failed for ${filePath}:`, err);
181
+ }
182
+ }
183
+
184
+ async flush(name: string, id: number, customOut?: string, customErr?: string) {
185
+ const paths = this.getLogPaths(name, id, customOut, customErr);
186
+ try { await Bun.write(paths.outFile, ""); } catch {}
187
+ try { await Bun.write(paths.errFile, ""); } catch {}
188
+ }
189
+
190
+ async checkRotation(
191
+ name: string,
192
+ id: number,
193
+ options: LogRotateOptions,
194
+ customOut?: string,
195
+ customErr?: string
196
+ ) {
197
+ const paths = this.getLogPaths(name, id, customOut, customErr);
198
+ await this.rotate(paths.outFile, options);
199
+ await this.rotate(paths.errFile, options);
200
+ }
201
+ }
@@ -0,0 +1,119 @@
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
+ import { MODULE_DIR } from "./constants";
19
+ import { existsSync, readdirSync } from "fs";
20
+ import type { ProcessManager } from "./process-manager";
21
+
22
+ export interface BM2Module {
23
+ name: string;
24
+ version: string;
25
+ init(pm: ProcessManager): void | Promise<void>;
26
+ destroy?(): void | Promise<void>;
27
+ }
28
+
29
+ export class ModuleManager {
30
+ private modules: Map<string, BM2Module> = new Map();
31
+ private pm: ProcessManager;
32
+
33
+ constructor(pm: ProcessManager) {
34
+ this.pm = pm;
35
+ }
36
+
37
+ async install(moduleNameOrPath: string): Promise<string> {
38
+ const targetDir = join(MODULE_DIR, moduleNameOrPath.replace(/[^a-zA-Z0-9_-]/g, "_"));
39
+
40
+ if (moduleNameOrPath.startsWith("http") || moduleNameOrPath.startsWith("git")) {
41
+ // Clone from git
42
+ const proc = Bun.spawn(["git", "clone", moduleNameOrPath, targetDir], {
43
+ stdout: "pipe", stderr: "pipe",
44
+ });
45
+ await proc.exited;
46
+ } else if (moduleNameOrPath.startsWith("/") || moduleNameOrPath.startsWith(".")) {
47
+ // Local path - symlink
48
+ const { symlinkSync } = require("fs");
49
+ symlinkSync(moduleNameOrPath, targetDir);
50
+ } else {
51
+ // npm package
52
+ const proc = Bun.spawn(["bun", "add", moduleNameOrPath], {
53
+ cwd: MODULE_DIR,
54
+ stdout: "pipe", stderr: "pipe",
55
+ });
56
+ await proc.exited;
57
+ }
58
+
59
+ // Install deps
60
+ if (existsSync(join(targetDir, "package.json"))) {
61
+ const proc = Bun.spawn(["bun", "install"], {
62
+ cwd: targetDir,
63
+ stdout: "pipe", stderr: "pipe",
64
+ });
65
+ await proc.exited;
66
+ }
67
+
68
+ // Load
69
+ await this.load(targetDir);
70
+ return targetDir;
71
+ }
72
+
73
+ async load(modulePath: string): Promise<void> {
74
+ try {
75
+ const pkg = await Bun.file(join(modulePath, "package.json")).json();
76
+ const main = pkg.main || pkg.module || "index.ts";
77
+ const mod: BM2Module = (await import(join(modulePath, main))).default;
78
+
79
+ if (!mod.name) mod.name = pkg.name;
80
+ if (!mod.version) mod.version = pkg.version;
81
+
82
+ await mod.init(this.pm);
83
+ this.modules.set(mod.name, mod);
84
+ console.log(`[bm2] Module loaded: ${mod.name}@${mod.version}`);
85
+ } catch (err: any) {
86
+ console.error(`[bm2] Failed to load module ${modulePath}:`, err.message);
87
+ }
88
+ }
89
+
90
+ async uninstall(name: string): Promise<void> {
91
+ const mod = this.modules.get(name);
92
+ if (mod?.destroy) await mod.destroy();
93
+ this.modules.delete(name);
94
+
95
+ const modPath = join(MODULE_DIR, name);
96
+ if (existsSync(modPath)) {
97
+ const { rmSync } = require("fs");
98
+ rmSync(modPath, { recursive: true, force: true });
99
+ }
100
+ }
101
+
102
+ async loadAll(): Promise<void> {
103
+ if (!existsSync(MODULE_DIR)) return;
104
+ const entries = readdirSync(MODULE_DIR);
105
+ for (const entry of entries) {
106
+ const modPath = join(MODULE_DIR, entry);
107
+ if (existsSync(join(modPath, "package.json"))) {
108
+ await this.load(modPath);
109
+ }
110
+ }
111
+ }
112
+
113
+ list(): Array<{ name: string; version: string }> {
114
+ return Array.from(this.modules.values()).map((m) => ({
115
+ name: m.name,
116
+ version: m.version,
117
+ }));
118
+ }
119
+ }