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/LICENSE +674 -0
- package/README.md +1591 -0
- package/package.json +68 -0
- package/src/cluster-manager.ts +117 -0
- package/src/constants.ts +44 -0
- package/src/cron-manager.ts +74 -0
- package/src/daemon.ts +233 -0
- package/src/dashboard-ui.ts +285 -0
- package/src/dashboard.ts +203 -0
- package/src/deploy.ts +188 -0
- package/src/env-manager.ts +77 -0
- package/src/graceful-reload.ts +71 -0
- package/src/health-checker.ts +112 -0
- package/src/index.ts +15 -0
- package/src/log-manager.ts +201 -0
- package/src/module-manager.ts +119 -0
- package/src/monitor.ts +185 -0
- package/src/process-container.ts +508 -0
- package/src/process-manager.ts +403 -0
- package/src/startup.ts +158 -0
- package/src/types.ts +230 -0
- package/src/utils.ts +171 -0
|
@@ -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
|
+
}
|