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,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
|
+
}
|