bm2 1.0.3 → 1.0.5

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