bm2 1.0.0 → 1.0.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bm2",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
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
5
  "main": "src/index.ts",
6
6
  "module": "src/index.ts",
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env bun
1
2
  /**
2
3
  * BM2 — Bun Process Manager
3
4
  * A production-grade process manager for Bun.
@@ -13,3 +14,1064 @@
13
14
  * License: GPL-3.0-only
14
15
  * Author: Zak <zak@maxxpainn.com>
15
16
  */
17
+
18
+ import { existsSync, readFileSync, unlinkSync } from "fs";
19
+ import { resolve, join, extname } from "path";
20
+ import {
21
+ APP_NAME,
22
+ VERSION,
23
+ DAEMON_SOCKET,
24
+ DAEMON_PID_FILE,
25
+ BM2_HOME,
26
+ DASHBOARD_PORT,
27
+ METRICS_PORT,
28
+ } from "./constants";
29
+ import { ensureDirs, formatBytes, formatUptime, colorize, padRight } from "./utils";
30
+ import { DeployManager } from "./deploy";
31
+ import { StartupManager } from "./startup-manager";
32
+ import { EnvManager } from "./env-manager";
33
+ import type {
34
+ DaemonMessage,
35
+ DaemonResponse,
36
+ StartOptions,
37
+ EcosystemConfig,
38
+ ProcessState,
39
+ } from "./types";
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Ensure directory structure exists
43
+ // ---------------------------------------------------------------------------
44
+ ensureDirs();
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Daemon communication helpers
48
+ // ---------------------------------------------------------------------------
49
+
50
+ function isDaemonRunning(): boolean {
51
+ if (!existsSync(DAEMON_PID_FILE)) return false;
52
+ try {
53
+ const pid = parseInt(readFileSync(DAEMON_PID_FILE, "utf-8").trim());
54
+ process.kill(pid, 0); // signal 0 — just check existence
55
+ return true;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ async function startDaemon(): Promise<void> {
62
+ if (isDaemonRunning()) return;
63
+
64
+ const daemonScript = join(import.meta.dir, "daemon.ts");
65
+ const bunPath = Bun.which("bun") || "bun";
66
+
67
+ const child = Bun.spawn([bunPath, "run", daemonScript], {
68
+ stdout: "ignore",
69
+ stderr: "ignore",
70
+ stdin: "ignore",
71
+ });
72
+
73
+ // Detach so the daemon outlives the CLI
74
+ child.unref();
75
+
76
+ // Wait for socket to appear
77
+ for (let i = 0; i < 50; i++) {
78
+ if (existsSync(DAEMON_SOCKET)) return;
79
+ await Bun.sleep(100);
80
+ }
81
+
82
+ throw new Error("Daemon failed to start (socket not found after 5 s)");
83
+ }
84
+
85
+ async function sendToDaemon(msg: DaemonMessage): Promise<DaemonResponse> {
86
+ await startDaemon();
87
+
88
+ return new Promise((resolvePromise, reject) => {
89
+ const id = crypto.randomUUID();
90
+ msg.id = id;
91
+
92
+ const timeout = setTimeout(() => {
93
+ reject(new Error("Daemon response timed out"));
94
+ }, 30_000);
95
+
96
+ const ws = new WebSocket(`ws+unix://${DAEMON_SOCKET}`);
97
+
98
+ ws.onopen = () => {
99
+ ws.send(JSON.stringify(msg));
100
+ };
101
+
102
+ ws.onmessage = (event) => {
103
+ try {
104
+ const res: DaemonResponse = JSON.parse(String(event.data));
105
+ if (res.id === id || !res.id) {
106
+ clearTimeout(timeout);
107
+ ws.close();
108
+ resolvePromise(res);
109
+ }
110
+ } catch (err: any) {
111
+ clearTimeout(timeout);
112
+ ws.close();
113
+ reject(err);
114
+ }
115
+ };
116
+
117
+ ws.onerror = (err) => {
118
+ clearTimeout(timeout);
119
+ reject(new Error(`WebSocket error: ${err}`));
120
+ };
121
+
122
+ ws.onclose = () => {
123
+ clearTimeout(timeout);
124
+ };
125
+ });
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Table rendering
130
+ // ---------------------------------------------------------------------------
131
+
132
+ function statusColor(status: string): string {
133
+ switch (status) {
134
+ case "online":
135
+ return "green";
136
+ case "stopped":
137
+ return "gray";
138
+ case "errored":
139
+ return "red";
140
+ case "launching":
141
+ case "waiting-restart":
142
+ return "yellow";
143
+ case "stopping":
144
+ return "magenta";
145
+ default:
146
+ return "white";
147
+ }
148
+ }
149
+
150
+ function printProcessTable(processes: ProcessState[]) {
151
+ if (!processes || processes.length === 0) {
152
+ console.log(colorize("No processes running", "dim"));
153
+ return;
154
+ }
155
+
156
+ const header = [
157
+ padRight("id", 4),
158
+ padRight("name", 20),
159
+ padRight("namespace", 12),
160
+ padRight("version", 10),
161
+ padRight("mode", 8),
162
+ padRight("pid", 8),
163
+ padRight("uptime", 10),
164
+ padRight("↺", 4),
165
+ padRight("status", 16),
166
+ padRight("cpu", 8),
167
+ padRight("mem", 10),
168
+ ].join(" ");
169
+
170
+ console.log(colorize(header, "dim"));
171
+ console.log(colorize("─".repeat(header.length), "dim"));
172
+
173
+ for (const p of processes) {
174
+ const uptime =
175
+ p.status === "online" ? formatUptime(Date.now() - p.pm2_env.pm_uptime) : "0s";
176
+
177
+ const row = [
178
+ padRight(String(p.pm_id), 4),
179
+ padRight(p.name, 20),
180
+ padRight(p.namespace || "default", 12),
181
+ padRight(p.pm2_env.version || "N/A", 10),
182
+ padRight(p.pm2_env.execMode, 8),
183
+ padRight(p.pid ? String(p.pid) : "N/A", 8),
184
+ padRight(uptime, 10),
185
+ padRight(String(p.pm2_env.restart_time), 4),
186
+ padRight(p.status, 16),
187
+ padRight(p.monit.cpu.toFixed(1) + "%", 8),
188
+ padRight(formatBytes(p.monit.memory), 10),
189
+ ];
190
+
191
+ const line = row.join(" ");
192
+ // Colorize the status cell inline
193
+ const colored = line.replace(
194
+ p.status,
195
+ colorize(p.status, statusColor(p.status))
196
+ );
197
+ console.log(colored);
198
+ }
199
+ }
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // Ecosystem config loader
203
+ // ---------------------------------------------------------------------------
204
+
205
+ async function loadEcosystemConfig(filePath: string): Promise<EcosystemConfig> {
206
+ const abs = resolve(filePath);
207
+ if (!existsSync(abs)) {
208
+ throw new Error(`Ecosystem file not found: ${abs}`);
209
+ }
210
+
211
+ const ext = extname(abs);
212
+ if (ext === ".json") {
213
+ return await Bun.file(abs).json();
214
+ }
215
+
216
+ // .ts, .js, .mjs — dynamic import
217
+ const mod = await import(abs);
218
+ return mod.default || mod;
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // Parse CLI flags into StartOptions
223
+ // ---------------------------------------------------------------------------
224
+
225
+ function parseStartFlags(args: string[], scriptOrConfig: string): StartOptions {
226
+ const opts: StartOptions = { script: scriptOrConfig };
227
+
228
+ let i = 0;
229
+ const positionalArgs: string[] = [];
230
+
231
+ while (i < args.length) {
232
+ const arg = args[i]!;
233
+
234
+ switch (arg) {
235
+ case "--name":
236
+ case "-n":
237
+ opts.name = args[++i];
238
+ break;
239
+ case "--instances":
240
+ case "-i":
241
+ opts.instances = parseInt(args[++i]!) || 1;
242
+ break;
243
+ case "--cwd":
244
+ opts.cwd = args[++i];
245
+ break;
246
+ case "--interpreter":
247
+ opts.interpreter = args[++i];
248
+ break;
249
+ case "--interpreter-args":
250
+ opts.interpreterArgs = args[++i]!.split(" ");
251
+ break;
252
+ case "--node-args":
253
+ opts.nodeArgs = args[++i]!.split(" ");
254
+ break;
255
+ case "--watch":
256
+ case "-w":
257
+ opts.watch = true;
258
+ break;
259
+ case "--watch-path":
260
+ if (!Array.isArray(opts.watch)) opts.watch = [];
261
+ (opts.watch as string[]).push(args[++i]!);
262
+ break;
263
+ case "--ignore-watch":
264
+ opts.ignoreWatch = args[++i]!.split(",");
265
+ break;
266
+ case "--exec-mode":
267
+ case "-x":
268
+ opts.execMode = args[++i] as "fork" | "cluster";
269
+ break;
270
+ case "--max-memory-restart":
271
+ opts.maxMemoryRestart = args[++i];
272
+ break;
273
+ case "--max-restarts":
274
+ opts.maxRestarts = parseInt(args[++i]!);
275
+ break;
276
+ case "--min-uptime":
277
+ opts.minUptime = parseInt(args[++i]!);
278
+ break;
279
+ case "--kill-timeout":
280
+ opts.killTimeout = parseInt(args[++i]!);
281
+ break;
282
+ case "--restart-delay":
283
+ opts.restartDelay = parseInt(args[++i]!);
284
+ break;
285
+ case "--cron":
286
+ case "--cron-restart":
287
+ opts.cron = args[++i];
288
+ break;
289
+ case "--no-autorestart":
290
+ opts.autorestart = false;
291
+ break;
292
+ case "--env": {
293
+ const envPair = args[++i]!;
294
+ const eqIdx = envPair.indexOf("=");
295
+ if (eqIdx !== -1) {
296
+ if (!opts.env) opts.env = {};
297
+ opts.env[envPair.substring(0, eqIdx)] = envPair.substring(eqIdx + 1);
298
+ }
299
+ break;
300
+ }
301
+ case "--log":
302
+ case "--output":
303
+ case "-o":
304
+ opts.outFile = args[++i];
305
+ break;
306
+ case "--error":
307
+ case "-e":
308
+ opts.errorFile = args[++i];
309
+ break;
310
+ case "--merge-logs":
311
+ opts.mergeLogs = true;
312
+ break;
313
+ case "--log-date-format":
314
+ opts.logDateFormat = args[++i];
315
+ break;
316
+ case "--log-max-size":
317
+ opts.logMaxSize = args[++i];
318
+ break;
319
+ case "--log-retain":
320
+ opts.logRetain = parseInt(args[++i]!);
321
+ break;
322
+ case "--log-compress":
323
+ opts.logCompress = true;
324
+ break;
325
+ case "--port":
326
+ case "-p":
327
+ opts.port = parseInt(args[++i]!);
328
+ break;
329
+ case "--health-check-url":
330
+ opts.healthCheckUrl = args[++i];
331
+ break;
332
+ case "--health-check-interval":
333
+ opts.healthCheckInterval = parseInt(args[++i]!);
334
+ break;
335
+ case "--health-check-timeout":
336
+ opts.healthCheckTimeout = parseInt(args[++i]!);
337
+ break;
338
+ case "--health-check-max-fails":
339
+ opts.healthCheckMaxFails = parseInt(args[++i]!);
340
+ break;
341
+ case "--wait-ready":
342
+ opts.waitReady = true;
343
+ break;
344
+ case "--listen-timeout":
345
+ opts.listenTimeout = parseInt(args[++i]!);
346
+ break;
347
+ case "--namespace":
348
+ opts.namespace = args[++i];
349
+ break;
350
+ case "--source-map-support":
351
+ opts.sourceMapSupport = true;
352
+ break;
353
+ case "--":
354
+ // Everything after -- is passed as script args
355
+ positionalArgs.push(...args.slice(i + 1));
356
+ i = args.length;
357
+ break;
358
+ default:
359
+ if (!arg.startsWith("-")) {
360
+ positionalArgs.push(arg);
361
+ }
362
+ break;
363
+ }
364
+ i++;
365
+ }
366
+
367
+ if (positionalArgs.length > 0) {
368
+ opts.args = positionalArgs;
369
+ }
370
+
371
+ return opts;
372
+ }
373
+
374
+ // ---------------------------------------------------------------------------
375
+ // Commands
376
+ // ---------------------------------------------------------------------------
377
+
378
+ async function cmdStart(args: string[]) {
379
+ const scriptOrConfig = args[0];
380
+ if (!scriptOrConfig) {
381
+ console.error(colorize("Usage: bm2 start <script|config> [options]", "red"));
382
+ process.exit(1);
383
+ }
384
+
385
+ const ext = extname(scriptOrConfig);
386
+
387
+ // Ecosystem file
388
+ if (
389
+ ext === ".json" ||
390
+ scriptOrConfig.includes("ecosystem") ||
391
+ scriptOrConfig.includes("bm2.config")
392
+ ) {
393
+ const config = await loadEcosystemConfig(scriptOrConfig);
394
+ const res = await sendToDaemon({ type: "ecosystem", data: config });
395
+ if (!res.success) {
396
+ console.error(colorize(`Error: ${res.error}`, "red"));
397
+ process.exit(1);
398
+ }
399
+ printProcessTable(res.data);
400
+ return;
401
+ }
402
+
403
+ // Single script
404
+ const opts = parseStartFlags(args.slice(1), resolve(scriptOrConfig));
405
+ opts.script = resolve(scriptOrConfig);
406
+
407
+ const res = await sendToDaemon({ type: "start", data: opts });
408
+ if (!res.success) {
409
+ console.error(colorize(`Error: ${res.error}`, "red"));
410
+ process.exit(1);
411
+ }
412
+ printProcessTable(res.data);
413
+ }
414
+
415
+ async function cmdStop(args: string[]) {
416
+ const target = args[0] || "all";
417
+ const type = target === "all" ? "stopAll" : "stop";
418
+ const data = target === "all" ? undefined : { target };
419
+
420
+ const res = await sendToDaemon({ type, data });
421
+ if (!res.success) {
422
+ console.error(colorize(`Error: ${res.error}`, "red"));
423
+ process.exit(1);
424
+ }
425
+ printProcessTable(res.data);
426
+ }
427
+
428
+ async function cmdRestart(args: string[]) {
429
+ const target = args[0] || "all";
430
+ const type = target === "all" ? "restartAll" : "restart";
431
+ const data = target === "all" ? undefined : { target };
432
+
433
+ const res = await sendToDaemon({ type, data });
434
+ if (!res.success) {
435
+ console.error(colorize(`Error: ${res.error}`, "red"));
436
+ process.exit(1);
437
+ }
438
+ printProcessTable(res.data);
439
+ }
440
+
441
+ async function cmdReload(args: string[]) {
442
+ const target = args[0] || "all";
443
+ const type = target === "all" ? "reloadAll" : "reload";
444
+ const data = target === "all" ? undefined : { target };
445
+
446
+ const res = await sendToDaemon({ type, data });
447
+ if (!res.success) {
448
+ console.error(colorize(`Error: ${res.error}`, "red"));
449
+ process.exit(1);
450
+ }
451
+ printProcessTable(res.data);
452
+ }
453
+
454
+ async function cmdDelete(args: string[]) {
455
+ const target = args[0] || "all";
456
+ const type = target === "all" ? "deleteAll" : "delete";
457
+ const data = target === "all" ? undefined : { target };
458
+
459
+ const res = await sendToDaemon({ type, data });
460
+ if (!res.success) {
461
+ console.error(colorize(`Error: ${res.error}`, "red"));
462
+ process.exit(1);
463
+ }
464
+ console.log(colorize("✓ Deleted", "green"));
465
+ printProcessTable(res.data);
466
+ }
467
+
468
+ async function cmdList() {
469
+ const res = await sendToDaemon({ type: "list" });
470
+ if (!res.success) {
471
+ console.error(colorize(`Error: ${res.error}`, "red"));
472
+ process.exit(1);
473
+ }
474
+ printProcessTable(res.data);
475
+ }
476
+
477
+ async function cmdDescribe(args: string[]) {
478
+ const target = args[0];
479
+ if (!target) {
480
+ console.error(colorize("Usage: bm2 describe <id|name>", "red"));
481
+ process.exit(1);
482
+ }
483
+
484
+ const res = await sendToDaemon({ type: "describe", data: { target } });
485
+ if (!res.success) {
486
+ console.error(colorize(`Error: ${res.error}`, "red"));
487
+ process.exit(1);
488
+ }
489
+
490
+ const processes: ProcessState[] = res.data;
491
+ for (const p of processes) {
492
+ console.log(colorize(`\n─── ${p.name} (id: ${p.pm_id}) ───`, "bold"));
493
+ console.log(` Status : ${colorize(p.status, statusColor(p.status))}`);
494
+ console.log(` PID : ${p.pid || "N/A"}`);
495
+ console.log(` Exec mode : ${p.pm2_env.execMode}`);
496
+ console.log(` Instances : ${p.pm2_env.instances}`);
497
+ console.log(` Namespace : ${p.namespace || "default"}`);
498
+ console.log(` Script : ${p.pm2_env.script}`);
499
+ console.log(` CWD : ${p.pm2_env.cwd}`);
500
+ console.log(` Args : ${p.pm2_env.args.join(" ") || "(none)"}`);
501
+ console.log(` Interpreter : ${p.pm2_env.interpreter || "bun"}`);
502
+ console.log(` Restarts : ${p.pm2_env.restart_time}`);
503
+ console.log(` Unstable : ${p.pm2_env.unstable_restarts}`);
504
+ console.log(
505
+ ` Uptime : ${
506
+ p.status === "online" ? formatUptime(Date.now() - p.pm2_env.pm_uptime) : "N/A"
507
+ }`
508
+ );
509
+ console.log(` Created at : ${new Date(p.pm2_env.created_at).toISOString()}`);
510
+ console.log(` CPU : ${p.monit.cpu.toFixed(1)}%`);
511
+ console.log(` Memory : ${formatBytes(p.monit.memory)}`);
512
+ if (p.monit.handles !== undefined)
513
+ console.log(` Handles : ${p.monit.handles}`);
514
+ if (p.monit.eventLoopLatency !== undefined)
515
+ console.log(` EL Latency : ${p.monit.eventLoopLatency.toFixed(2)} ms`);
516
+ console.log(` Watch : ${p.pm2_env.watch}`);
517
+ console.log(` Autorestart : ${p.pm2_env.autorestart}`);
518
+ console.log(` Max restarts : ${p.pm2_env.maxRestarts}`);
519
+ console.log(` Kill timeout : ${p.pm2_env.killTimeout} ms`);
520
+ if (p.pm2_env.healthCheckUrl)
521
+ console.log(` Health URL : ${p.pm2_env.healthCheckUrl}`);
522
+ if (p.pm2_env.cronRestart)
523
+ console.log(` Cron restart : ${p.pm2_env.cronRestart}`);
524
+ if (p.pm2_env.port)
525
+ console.log(` Port : ${p.pm2_env.port}`);
526
+ console.log();
527
+ }
528
+ }
529
+
530
+ async function cmdLogs(args: string[]) {
531
+ const target = args[0] || "all";
532
+ let lines = 20;
533
+ const linesIdx = args.indexOf("--lines");
534
+ if (linesIdx !== -1 && args[linesIdx + 1]) {
535
+ lines = parseInt(args[linesIdx + 1]!);
536
+ }
537
+
538
+ const res = await sendToDaemon({ type: "logs", data: { target, lines } });
539
+ if (!res.success) {
540
+ console.error(colorize(`Error: ${res.error}`, "red"));
541
+ process.exit(1);
542
+ }
543
+
544
+ for (const log of res.data) {
545
+ console.log(colorize(`\n─── ${log.name} (id: ${log.id}) ───`, "bold"));
546
+ if (log.out) {
547
+ console.log(colorize("--- stdout ---", "dim"));
548
+ console.log(log.out);
549
+ }
550
+ if (log.err) {
551
+ console.log(colorize("--- stderr ---", "red"));
552
+ console.log(log.err);
553
+ }
554
+ }
555
+ }
556
+
557
+ async function cmdFlush(args: string[]) {
558
+ const target = args[0];
559
+ const res = await sendToDaemon({ type: "flush", data: target ? { target } : undefined });
560
+ if (!res.success) {
561
+ console.error(colorize(`Error: ${res.error}`, "red"));
562
+ process.exit(1);
563
+ }
564
+ console.log(colorize("✓ Logs flushed", "green"));
565
+ }
566
+
567
+ async function cmdScale(args: string[]) {
568
+ const target = args[0];
569
+ const count = parseInt(args[1]!);
570
+ if (!target || isNaN(count)) {
571
+ console.error(colorize("Usage: bm2 scale <name|id> <count>", "red"));
572
+ process.exit(1);
573
+ }
574
+
575
+ const res = await sendToDaemon({ type: "scale", data: { target, count } });
576
+ if (!res.success) {
577
+ console.error(colorize(`Error: ${res.error}`, "red"));
578
+ process.exit(1);
579
+ }
580
+ printProcessTable(res.data);
581
+ }
582
+
583
+ async function cmdSave() {
584
+ const res = await sendToDaemon({ type: "save" });
585
+ if (!res.success) {
586
+ console.error(colorize(`Error: ${res.error}`, "red"));
587
+ process.exit(1);
588
+ }
589
+ console.log(colorize("✓ Process list saved", "green"));
590
+ }
591
+
592
+ async function cmdResurrect() {
593
+ const res = await sendToDaemon({ type: "resurrect" });
594
+ if (!res.success) {
595
+ console.error(colorize(`Error: ${res.error}`, "red"));
596
+ process.exit(1);
597
+ }
598
+ printProcessTable(res.data);
599
+ }
600
+
601
+ async function cmdSignal(args: string[]) {
602
+ const signal = args[0];
603
+ const target = args[1];
604
+ if (!signal || !target) {
605
+ console.error(colorize("Usage: bm2 sendSignal <signal> <id|name>", "red"));
606
+ process.exit(1);
607
+ }
608
+
609
+ const res = await sendToDaemon({ type: "signal", data: { target, signal } });
610
+ if (!res.success) {
611
+ console.error(colorize(`Error: ${res.error}`, "red"));
612
+ process.exit(1);
613
+ }
614
+ console.log(colorize(`✓ Signal ${signal} sent to ${target}`, "green"));
615
+ }
616
+
617
+ async function cmdReset(args: string[]) {
618
+ const target = args[0] || "all";
619
+ const res = await sendToDaemon({ type: "reset", data: { target } });
620
+ if (!res.success) {
621
+ console.error(colorize(`Error: ${res.error}`, "red"));
622
+ process.exit(1);
623
+ }
624
+ console.log(colorize("✓ Restart counters reset", "green"));
625
+ printProcessTable(res.data);
626
+ }
627
+
628
+ async function cmdMonit() {
629
+ const res = await sendToDaemon({ type: "metrics" });
630
+ if (!res.success) {
631
+ console.error(colorize(`Error: ${res.error}`, "red"));
632
+ process.exit(1);
633
+ }
634
+
635
+ const snapshot = res.data;
636
+ console.log(colorize("\n⚡ BM2 Monitor\n", "bold"));
637
+
638
+ console.log(colorize("System:", "cyan"));
639
+ console.log(` Platform : ${snapshot.system.platform}`);
640
+ console.log(` CPUs : ${snapshot.system.cpuCount}`);
641
+ console.log(` Memory : ${formatBytes(snapshot.system.totalMemory - snapshot.system.freeMemory)} / ${formatBytes(snapshot.system.totalMemory)}`);
642
+ console.log(` Load avg : ${snapshot.system.loadAvg.map((l: number) => l.toFixed(2)).join(", ")}`);
643
+ console.log();
644
+
645
+ console.log(colorize("Processes:", "cyan"));
646
+ for (const p of snapshot.processes) {
647
+ const statusStr = colorize(padRight(p.status, 14), statusColor(p.status));
648
+ console.log(
649
+ ` ${padRight(String(p.id), 4)} ${padRight(p.name, 20)} ${statusStr} CPU: ${padRight(p.cpu.toFixed(1) + "%", 8)} MEM: ${padRight(formatBytes(p.memory), 10)} ↺ ${p.restarts}`
650
+ );
651
+ }
652
+ console.log();
653
+ }
654
+
655
+ async function cmdDashboard(args: string[]) {
656
+ let port = DASHBOARD_PORT;
657
+ let metricsPort = METRICS_PORT;
658
+
659
+ const portIdx = args.indexOf("--port");
660
+ if (portIdx !== -1 && args[portIdx + 1]) port = parseInt(args[portIdx + 1]!);
661
+ const mIdx = args.indexOf("--metrics-port");
662
+ if (mIdx !== -1 && args[mIdx + 1]) metricsPort = parseInt(args[mIdx + 1]!);
663
+
664
+ const res = await sendToDaemon({ type: "dashboard", data: { port, metricsPort } });
665
+ if (!res.success) {
666
+ console.error(colorize(`Error: ${res.error}`, "red"));
667
+ process.exit(1);
668
+ }
669
+ console.log(colorize(`✓ Dashboard running at http://localhost:${res.data.port}`, "green"));
670
+ console.log(colorize(` Prometheus metrics at http://localhost:${res.data.metricsPort}/metrics`, "dim"));
671
+ }
672
+
673
+ async function cmdDashboardStop() {
674
+ const res = await sendToDaemon({ type: "dashboardStop" });
675
+ if (!res.success) {
676
+ console.error(colorize(`Error: ${res.error}`, "red"));
677
+ process.exit(1);
678
+ }
679
+ console.log(colorize("✓ Dashboard stopped", "green"));
680
+ }
681
+
682
+ async function cmdPing() {
683
+ try {
684
+ const res = await sendToDaemon({ type: "ping" });
685
+ if (res.success) {
686
+ console.log(colorize("✓ Daemon is alive", "green"));
687
+ console.log(` PID : ${res.data.pid}`);
688
+ console.log(` Uptime : ${formatUptime(res.data.uptime * 1000)}`);
689
+ } else {
690
+ console.log(colorize("✗ Daemon responded with error", "red"));
691
+ }
692
+ } catch {
693
+ console.log(colorize("✗ Daemon is not running", "red"));
694
+ }
695
+ }
696
+
697
+ async function cmdKill() {
698
+ try {
699
+ await sendToDaemon({ type: "kill" });
700
+ } catch {
701
+ // Expected — daemon exits
702
+ }
703
+
704
+ // Clean up leftover files
705
+ try {
706
+ if (existsSync(DAEMON_SOCKET)) unlinkSync(DAEMON_SOCKET);
707
+ } catch {}
708
+ try {
709
+ if (existsSync(DAEMON_PID_FILE)) unlinkSync(DAEMON_PID_FILE);
710
+ } catch {}
711
+
712
+ console.log(colorize("✓ Daemon killed", "green"));
713
+ }
714
+
715
+ async function cmdDeploy(args: string[]) {
716
+ const configFile = args[0];
717
+ const environment = args[1];
718
+
719
+ if (!configFile || !environment) {
720
+ console.error(colorize("Usage: bm2 deploy <config> <environment> [setup]", "red"));
721
+ process.exit(1);
722
+ }
723
+
724
+ const config = await loadEcosystemConfig(configFile);
725
+ if (!config.deploy || !config.deploy[environment]) {
726
+ console.error(colorize(`Deploy environment "${environment}" not found in config`, "red"));
727
+ process.exit(1);
728
+ }
729
+
730
+ const deployConfig = config.deploy[environment]!;
731
+ const deployer = new DeployManager();
732
+
733
+ if (args[2] === "setup") {
734
+ await deployer.setup(deployConfig);
735
+ } else {
736
+ await deployer.deploy(deployConfig, args[2]);
737
+ }
738
+ }
739
+
740
+ async function cmdStartup(args: string[]) {
741
+ const startup = new StartupManager();
742
+
743
+ if (args[0] === "remove" || args[0] === "uninstall") {
744
+ const result = await startup.uninstall();
745
+ console.log(result);
746
+ return;
747
+ }
748
+
749
+ if (args[0] === "install") {
750
+ const result = await startup.install();
751
+ console.log(result);
752
+ return;
753
+ }
754
+
755
+ // Just print the config
756
+ const content = await startup.generate(args[0]);
757
+ console.log(content);
758
+ }
759
+
760
+ async function cmdEnv(args: string[]) {
761
+ const envMgr = new EnvManager();
762
+ const subCmd = args[0];
763
+
764
+ switch (subCmd) {
765
+ case "set": {
766
+ const name = args[1];
767
+ const key = args[2];
768
+ const value = args[3];
769
+ if (!name || !key || value === undefined) {
770
+ console.error(colorize("Usage: bm2 env set <name> <key> <value>", "red"));
771
+ process.exit(1);
772
+ }
773
+ await envMgr.setEnv(name, key, value);
774
+ console.log(colorize(`✓ Set ${key}=${value} for ${name}`, "green"));
775
+ break;
776
+ }
777
+ case "get": {
778
+ const name = args[1];
779
+ if (!name) {
780
+ console.error(colorize("Usage: bm2 env get <name>", "red"));
781
+ process.exit(1);
782
+ }
783
+ const env = await envMgr.getEnv(name);
784
+ for (const [k, v] of Object.entries(env)) {
785
+ console.log(`${colorize(k, "cyan")}=${v}`);
786
+ }
787
+ break;
788
+ }
789
+ case "delete":
790
+ case "rm": {
791
+ const name = args[1];
792
+ const key = args[2];
793
+ if (!name) {
794
+ console.error(colorize("Usage: bm2 env delete <name> [key]", "red"));
795
+ process.exit(1);
796
+ }
797
+ await envMgr.deleteEnv(name, key);
798
+ console.log(colorize(`✓ Deleted`, "green"));
799
+ break;
800
+ }
801
+ case "list": {
802
+ const all = await envMgr.getEnvs();
803
+ for (const [name, env] of Object.entries(all)) {
804
+ console.log(colorize(`\n${name}:`, "bold"));
805
+ for (const [k, v] of Object.entries(env)) {
806
+ console.log(` ${colorize(k, "cyan")}=${v}`);
807
+ }
808
+ }
809
+ break;
810
+ }
811
+ default:
812
+ console.error(colorize("Usage: bm2 env <set|get|delete|list> ...", "red"));
813
+ process.exit(1);
814
+ }
815
+ }
816
+
817
+ async function cmdModule(args: string[]) {
818
+ const subCmd = args[0];
819
+
820
+ switch (subCmd) {
821
+ case "install": {
822
+ const mod = args[1];
823
+ if (!mod) {
824
+ console.error(colorize("Usage: bm2 module install <name|url|path>", "red"));
825
+ process.exit(1);
826
+ }
827
+ const res = await sendToDaemon({ type: "moduleInstall", data: { module: mod } });
828
+ if (!res.success) {
829
+ console.error(colorize(`Error: ${res.error}`, "red"));
830
+ process.exit(1);
831
+ }
832
+ console.log(colorize(`✓ Module installed at ${res.data.path}`, "green"));
833
+ break;
834
+ }
835
+ case "uninstall":
836
+ case "remove": {
837
+ const mod = args[1];
838
+ if (!mod) {
839
+ console.error(colorize("Usage: bm2 module uninstall <name>", "red"));
840
+ process.exit(1);
841
+ }
842
+ const res = await sendToDaemon({ type: "moduleUninstall", data: { module: mod } });
843
+ if (!res.success) {
844
+ console.error(colorize(`Error: ${res.error}`, "red"));
845
+ process.exit(1);
846
+ }
847
+ console.log(colorize("✓ Module uninstalled", "green"));
848
+ break;
849
+ }
850
+ case "list":
851
+ case "ls": {
852
+ const res = await sendToDaemon({ type: "moduleList" });
853
+ if (!res.success) {
854
+ console.error(colorize(`Error: ${res.error}`, "red"));
855
+ process.exit(1);
856
+ }
857
+ if (res.data.length === 0) {
858
+ console.log(colorize("No modules installed", "dim"));
859
+ } else {
860
+ for (const m of res.data) {
861
+ console.log(` ${colorize(m.name, "cyan")} @ ${m.version}`);
862
+ }
863
+ }
864
+ break;
865
+ }
866
+ default:
867
+ console.error(colorize("Usage: bm2 module <install|uninstall|list> ...", "red"));
868
+ process.exit(1);
869
+ }
870
+ }
871
+
872
+ async function cmdPrometheus() {
873
+ const res = await sendToDaemon({ type: "prometheus" });
874
+ if (!res.success) {
875
+ console.error(colorize(`Error: ${res.error}`, "red"));
876
+ process.exit(1);
877
+ }
878
+ console.log(res.data);
879
+ }
880
+
881
+ // ---------------------------------------------------------------------------
882
+ // Help
883
+ // ---------------------------------------------------------------------------
884
+
885
+ function printHelp() {
886
+ console.log(`
887
+ ${colorize("⚡ BM2", "bold")} ${colorize(`v${VERSION}`, "dim")} — Bun Process Manager
888
+
889
+ ${colorize("Usage:", "bold")} bm2 <command> [options]
890
+
891
+ ${colorize("Process Management:", "cyan")}
892
+ start <script|config> [opts] Start a process or ecosystem config
893
+ stop [id|name|all] Stop process(es)
894
+ restart [id|name|all] Restart process(es)
895
+ reload [id|name|all] Graceful zero-downtime reload
896
+ delete [id|name|all] Stop and remove process(es)
897
+ scale <id|name> <count> Scale to N instances
898
+ list | ls | status List all processes
899
+ describe <id|name> Show detailed process info
900
+ reset <id|name|all> Reset restart counters
901
+
902
+ ${colorize("Logs:", "cyan")}
903
+ logs [id|name|all] [--lines N] Show recent logs
904
+ flush [id|name] Clear log files
905
+
906
+ ${colorize("Monitoring:", "cyan")}
907
+ monit Show live metrics snapshot
908
+ dashboard [--port N] Start web dashboard
909
+ dashboard stop Stop web dashboard
910
+ prometheus Print Prometheus metrics
911
+
912
+ ${colorize("Persistence:", "cyan")}
913
+ save Save current process list
914
+ resurrect Restore saved process list
915
+ startup [install|remove] Generate/install startup script
916
+
917
+ ${colorize("Deploy:", "cyan")}
918
+ deploy <config> <env> [setup] Deploy using ecosystem config
919
+
920
+ ${colorize("Environment:", "cyan")}
921
+ env set <name> <key> <val> Set env variable
922
+ env get <name> List env vars for a process
923
+ env delete <name> [key] Delete env variable(s)
924
+ env list List all env registries
925
+
926
+ ${colorize("Modules:", "cyan")}
927
+ module install <name|url> Install a BM2 module
928
+ module uninstall <name> Remove a module
929
+ module list List installed modules
930
+
931
+ ${colorize("Daemon:", "cyan")}
932
+ ping Check if daemon is alive
933
+ kill Kill the daemon and all processes
934
+ sendSignal <sig> <id|name> Send OS signal to process
935
+
936
+ ${colorize("Start Options:", "dim")}
937
+ --name, -n <name> Process name
938
+ --instances, -i <N> Number of instances (cluster)
939
+ --exec-mode, -x <mode> fork or cluster
940
+ --watch, -w Watch for file changes
941
+ --cwd <path> Working directory
942
+ --interpreter <bin> Custom interpreter
943
+ --node-args <args> Extra runtime arguments
944
+ --max-memory-restart <size> e.g. 200M, 1G
945
+ --max-restarts <N> Max restart attempts
946
+ --cron, --cron-restart <expr> Cron-based restart schedule
947
+ --port, -p <port> Base port for cluster
948
+ --env <KEY=VALUE> Set environment variable
949
+ --no-autorestart Disable auto-restart
950
+ --log, -o <file> Custom stdout log path
951
+ --error, -e <file> Custom stderr log path
952
+ --namespace <ns> Namespace grouping
953
+ --wait-ready Wait for ready signal
954
+ --health-check-url <url> HTTP health check endpoint
955
+ -- <args...> Pass arguments to script
956
+
957
+ ${colorize("Examples:", "dim")}
958
+ bm2 start app.ts
959
+ bm2 start server.ts --name api -i 4 --watch
960
+ bm2 start ecosystem.config.ts
961
+ bm2 restart api
962
+ bm2 scale api 8
963
+ bm2 logs api --lines 100
964
+ bm2 monit
965
+ bm2 save && bm2 resurrect
966
+ `);
967
+ }
968
+
969
+ // ---------------------------------------------------------------------------
970
+ // Main dispatch
971
+ // ---------------------------------------------------------------------------
972
+
973
+ const args = process.argv.slice(2);
974
+ const command = args[0];
975
+ const commandArgs = args.slice(1);
976
+
977
+ switch (command) {
978
+ case "start":
979
+ await cmdStart(commandArgs);
980
+ break;
981
+ case "stop":
982
+ await cmdStop(commandArgs);
983
+ break;
984
+ case "restart":
985
+ await cmdRestart(commandArgs);
986
+ break;
987
+ case "reload":
988
+ await cmdReload(commandArgs);
989
+ break;
990
+ case "delete":
991
+ case "del":
992
+ case "rm":
993
+ await cmdDelete(commandArgs);
994
+ break;
995
+ case "scale":
996
+ await cmdScale(commandArgs);
997
+ break;
998
+ case "list":
999
+ case "ls":
1000
+ case "status":
1001
+ await cmdList();
1002
+ break;
1003
+ case "describe":
1004
+ case "show":
1005
+ case "info":
1006
+ await cmdDescribe(commandArgs);
1007
+ break;
1008
+ case "logs":
1009
+ case "log":
1010
+ await cmdLogs(commandArgs);
1011
+ break;
1012
+ case "flush":
1013
+ await cmdFlush(commandArgs);
1014
+ break;
1015
+ case "monit":
1016
+ case "monitor":
1017
+ await cmdMonit();
1018
+ break;
1019
+ case "dashboard":
1020
+ if (commandArgs[0] === "stop") {
1021
+ await cmdDashboardStop();
1022
+ } else {
1023
+ await cmdDashboard(commandArgs);
1024
+ }
1025
+ break;
1026
+ case "prometheus":
1027
+ await cmdPrometheus();
1028
+ break;
1029
+ case "save":
1030
+ case "dump":
1031
+ await cmdSave();
1032
+ break;
1033
+ case "resurrect":
1034
+ case "restore":
1035
+ await cmdResurrect();
1036
+ break;
1037
+ case "reset":
1038
+ await cmdReset(commandArgs);
1039
+ break;
1040
+ case "sendSignal":
1041
+ case "signal":
1042
+ await cmdSignal(commandArgs);
1043
+ break;
1044
+ case "ping":
1045
+ await cmdPing();
1046
+ break;
1047
+ case "kill":
1048
+ await cmdKill();
1049
+ break;
1050
+ case "deploy":
1051
+ await cmdDeploy(commandArgs);
1052
+ break;
1053
+ case "startup":
1054
+ await cmdStartup(commandArgs);
1055
+ break;
1056
+ case "env":
1057
+ await cmdEnv(commandArgs);
1058
+ break;
1059
+ case "module":
1060
+ await cmdModule(commandArgs);
1061
+ break;
1062
+ case "version":
1063
+ case "-v":
1064
+ case "--version":
1065
+ console.log(`${APP_NAME} v${VERSION}`);
1066
+ break;
1067
+ case "help":
1068
+ case "-h":
1069
+ case "--help":
1070
+ case undefined:
1071
+ printHelp();
1072
+ break;
1073
+ default:
1074
+ console.error(colorize(`Unknown command: ${command}`, "red"));
1075
+ console.error(`Run ${colorize("bm2 --help", "cyan")} for usage information.`);
1076
+ process.exit(1);
1077
+ }
File without changes