bm2 1.0.1 → 1.0.3

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.
Files changed (3) hide show
  1. package/package.json +5 -2
  2. package/src/hello.js +2 -0
  3. package/src/index.ts +251 -741
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bm2",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
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",
@@ -59,9 +59,12 @@
59
59
  "linux",
60
60
  "darwin"
61
61
  ],
62
- "dependencies": {},
62
+ "dependencies": {
63
+ "ws": "^8.19.0"
64
+ },
63
65
  "devDependencies": {
64
66
  "@types/bun": "^1.3.9",
67
+ "@types/ws": "^8.18.1",
65
68
  "bun-types": "latest",
66
69
  "typescript": "^5.9.3"
67
70
  }
package/src/hello.js ADDED
@@ -0,0 +1,2 @@
1
+
2
+ console.log("Hello ===>")
package/src/index.ts CHANGED
@@ -2,21 +2,11 @@
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>
16
5
  */
17
6
 
18
- import { existsSync, readFileSync, unlinkSync } from "fs";
7
+ import { existsSync, readFileSync, unlinkSync, openSync } from "fs";
19
8
  import { resolve, join, extname } from "path";
9
+ import { createConnection } from "net";
20
10
  import {
21
11
  APP_NAME,
22
12
  VERSION,
@@ -31,8 +21,6 @@ import { DeployManager } from "./deploy";
31
21
  import { StartupManager } from "./startup-manager";
32
22
  import { EnvManager } from "./env-manager";
33
23
  import type {
34
- DaemonMessage,
35
- DaemonResponse,
36
24
  StartOptions,
37
25
  EcosystemConfig,
38
26
  ProcessState,
@@ -44,13 +32,34 @@ import type {
44
32
  ensureDirs();
45
33
 
46
34
  // ---------------------------------------------------------------------------
47
- // Daemon communication helpers
35
+ // Helpers
48
36
  // ---------------------------------------------------------------------------
49
37
 
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
+ */
50
58
  function isDaemonRunning(): boolean {
51
59
  if (!existsSync(DAEMON_PID_FILE)) return false;
52
60
  try {
53
61
  const pid = parseInt(readFileSync(DAEMON_PID_FILE, "utf-8").trim());
62
+ if (isNaN(pid)) return false;
54
63
  process.kill(pid, 0); // signal 0 — just check existence
55
64
  return true;
56
65
  } catch {
@@ -58,92 +67,182 @@ function isDaemonRunning(): boolean {
58
67
  }
59
68
  }
60
69
 
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
+ */
61
86
  async function startDaemon(): Promise<void> {
62
- if (isDaemonRunning()) return;
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
106
 
64
107
  const daemonScript = join(import.meta.dir, "daemon.ts");
65
108
  const bunPath = Bun.which("bun") || "bun";
66
109
 
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
67
123
  const child = Bun.spawn([bunPath, "run", daemonScript], {
68
- stdout: "ignore",
69
- stderr: "ignore",
70
124
  stdin: "ignore",
125
+ stdout: outFd,
126
+ stderr: errFd,
127
+ detached: true,
71
128
  });
72
129
 
73
- // Detach so the daemon outlives the CLI
74
130
  child.unref();
75
131
 
76
- // Wait for socket to appear
77
- for (let i = 0; i < 50; i++) {
132
+ // Wait for socket
133
+ const maxRetries = 50; // 5 seconds
134
+ for (let i = 0; i < maxRetries; i++) {
78
135
  if (existsSync(DAEMON_SOCKET)) return;
79
136
  await Bun.sleep(100);
80
137
  }
81
138
 
82
- throw new Error("Daemon failed to start (socket not found after 5 s)");
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
147
  }
84
148
 
85
- async function sendToDaemon(msg: DaemonMessage): Promise<DaemonResponse> {
149
+ /**
150
+ * Send a JSON message to the daemon and wait for a response.
151
+ */
152
+ async function sendToDaemon(message: object): Promise<any> {
86
153
  await startDaemon();
87
154
 
88
- return new Promise((resolvePromise, reject) => {
89
- const id = crypto.randomUUID();
90
- msg.id = id;
155
+ return new Promise((resolve, reject) => {
156
+ let settled = false;
91
157
 
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
- };
158
+ function settle(fn: typeof resolve | typeof reject, value: any) {
159
+ if (settled) return;
160
+ settled = true;
161
+ clearTimeout(timeout);
162
+ fn(value);
163
+ }
101
164
 
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);
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
109
188
  }
110
- } catch (err: any) {
111
- clearTimeout(timeout);
112
- ws.close();
113
- reject(err);
114
189
  }
115
- };
116
-
117
- ws.onerror = (err) => {
118
- clearTimeout(timeout);
119
- reject(new Error(`WebSocket error: ${err}`));
120
- };
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
+ }
121
202
 
122
- ws.onclose = () => {
123
- clearTimeout(timeout);
124
- };
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
+ }
229
+ });
125
230
  });
126
231
  }
127
232
 
128
233
  // ---------------------------------------------------------------------------
129
- // Table rendering
234
+ // Table Rendering & Utilities
130
235
  // ---------------------------------------------------------------------------
131
236
 
132
237
  function statusColor(status: string): string {
133
238
  switch (status) {
134
- case "online":
135
- return "green";
136
- case "stopped":
137
- return "gray";
138
- case "errored":
139
- return "red";
239
+ case "online": return "green";
240
+ case "stopped": return "gray";
241
+ case "errored": return "red";
140
242
  case "launching":
141
- case "waiting-restart":
142
- return "yellow";
143
- case "stopping":
144
- return "magenta";
145
- default:
146
- return "white";
243
+ case "waiting-restart": return "yellow";
244
+ case "stopping": return "magenta";
245
+ default: return "white";
147
246
  }
148
247
  }
149
248
 
@@ -157,7 +256,7 @@ function printProcessTable(processes: ProcessState[]) {
157
256
  padRight("id", 4),
158
257
  padRight("name", 20),
159
258
  padRight("namespace", 12),
160
- padRight("version", 10),
259
+ padRight("ver", 8),
161
260
  padRight("mode", 8),
162
261
  padRight("pid", 8),
163
262
  padRight("uptime", 10),
@@ -171,14 +270,12 @@ function printProcessTable(processes: ProcessState[]) {
171
270
  console.log(colorize("─".repeat(header.length), "dim"));
172
271
 
173
272
  for (const p of processes) {
174
- const uptime =
175
- p.status === "online" ? formatUptime(Date.now() - p.pm2_env.pm_uptime) : "0s";
176
-
273
+ const uptime = p.status === "online" ? formatUptime(Date.now() - p.pm2_env.pm_uptime) : "0s";
177
274
  const row = [
178
275
  padRight(String(p.pm_id), 4),
179
276
  padRight(p.name, 20),
180
277
  padRight(p.namespace || "default", 12),
181
- padRight(p.pm2_env.version || "N/A", 10),
278
+ padRight(p.pm2_env.version || "N/A", 8),
182
279
  padRight(p.pm2_env.execMode, 8),
183
280
  padRight(p.pid ? String(p.pid) : "N/A", 8),
184
281
  padRight(uptime, 10),
@@ -187,187 +284,71 @@ function printProcessTable(processes: ProcessState[]) {
187
284
  padRight(p.monit.cpu.toFixed(1) + "%", 8),
188
285
  padRight(formatBytes(p.monit.memory), 10),
189
286
  ];
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);
287
+ console.log(row.join(" ").replace(p.status, colorize(p.status, statusColor(p.status))));
198
288
  }
199
289
  }
200
290
 
201
- // ---------------------------------------------------------------------------
202
- // Ecosystem config loader
203
- // ---------------------------------------------------------------------------
204
-
205
291
  async function loadEcosystemConfig(filePath: string): Promise<EcosystemConfig> {
206
292
  const abs = resolve(filePath);
207
- if (!existsSync(abs)) {
208
- throw new Error(`Ecosystem file not found: ${abs}`);
209
- }
210
-
293
+ if (!existsSync(abs)) throw new Error(`Ecosystem file not found: ${abs}`);
211
294
  const ext = extname(abs);
212
- if (ext === ".json") {
213
- return await Bun.file(abs).json();
214
- }
215
-
216
- // .ts, .js, .mjs — dynamic import
295
+ if (ext === ".json") return await Bun.file(abs).json();
217
296
  const mod = await import(abs);
218
297
  return mod.default || mod;
219
298
  }
220
299
 
221
- // ---------------------------------------------------------------------------
222
- // Parse CLI flags into StartOptions
223
- // ---------------------------------------------------------------------------
224
-
225
300
  function parseStartFlags(args: string[], scriptOrConfig: string): StartOptions {
226
301
  const opts: StartOptions = { script: scriptOrConfig };
227
-
228
302
  let i = 0;
229
303
  const positionalArgs: string[] = [];
230
304
 
231
305
  while (i < args.length) {
232
306
  const arg = args[i]!;
233
-
234
307
  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":
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":
260
316
  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;
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;
292
328
  case "--env": {
293
- const envPair = args[++i]!;
294
- const eqIdx = envPair.indexOf("=");
295
- if (eqIdx !== -1) {
329
+ const p = args[++i]!;
330
+ const idx = p.indexOf("=");
331
+ if (idx !== -1) {
296
332
  if (!opts.env) opts.env = {};
297
- opts.env[envPair.substring(0, eqIdx)] = envPair.substring(eqIdx + 1);
333
+ opts.env[p.substring(0, idx)] = p.substring(idx + 1);
298
334
  }
299
335
  break;
300
336
  }
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;
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;
363
348
  }
364
349
  i++;
365
350
  }
366
-
367
- if (positionalArgs.length > 0) {
368
- opts.args = positionalArgs;
369
- }
370
-
351
+ if (positionalArgs.length > 0) opts.args = positionalArgs;
371
352
  return opts;
372
353
  }
373
354
 
@@ -382,14 +363,14 @@ async function cmdStart(args: string[]) {
382
363
  process.exit(1);
383
364
  }
384
365
 
385
- const ext = extname(scriptOrConfig);
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
+ }
386
371
 
387
- // Ecosystem file
388
- if (
389
- ext === ".json" ||
390
- scriptOrConfig.includes("ecosystem") ||
391
- scriptOrConfig.includes("bm2.config")
392
- ) {
372
+ const ext = extname(scriptOrConfig);
373
+ if (ext === ".json" || scriptOrConfig.includes("ecosystem") || scriptOrConfig.includes("bm2.config")) {
393
374
  const config = await loadEcosystemConfig(scriptOrConfig);
394
375
  const res = await sendToDaemon({ type: "ecosystem", data: config });
395
376
  if (!res.success) {
@@ -400,7 +381,6 @@ async function cmdStart(args: string[]) {
400
381
  return;
401
382
  }
402
383
 
403
- // Single script
404
384
  const opts = parseStartFlags(args.slice(1), resolve(scriptOrConfig));
405
385
  opts.script = resolve(scriptOrConfig);
406
386
 
@@ -415,9 +395,7 @@ async function cmdStart(args: string[]) {
415
395
  async function cmdStop(args: string[]) {
416
396
  const target = args[0] || "all";
417
397
  const type = target === "all" ? "stopAll" : "stop";
418
- const data = target === "all" ? undefined : { target };
419
-
420
- const res = await sendToDaemon({ type, data });
398
+ const res = await sendToDaemon({ type, data: target === "all" ? undefined : { target } });
421
399
  if (!res.success) {
422
400
  console.error(colorize(`Error: ${res.error}`, "red"));
423
401
  process.exit(1);
@@ -428,9 +406,7 @@ async function cmdStop(args: string[]) {
428
406
  async function cmdRestart(args: string[]) {
429
407
  const target = args[0] || "all";
430
408
  const type = target === "all" ? "restartAll" : "restart";
431
- const data = target === "all" ? undefined : { target };
432
-
433
- const res = await sendToDaemon({ type, data });
409
+ const res = await sendToDaemon({ type, data: target === "all" ? undefined : { target } });
434
410
  if (!res.success) {
435
411
  console.error(colorize(`Error: ${res.error}`, "red"));
436
412
  process.exit(1);
@@ -441,9 +417,7 @@ async function cmdRestart(args: string[]) {
441
417
  async function cmdReload(args: string[]) {
442
418
  const target = args[0] || "all";
443
419
  const type = target === "all" ? "reloadAll" : "reload";
444
- const data = target === "all" ? undefined : { target };
445
-
446
- const res = await sendToDaemon({ type, data });
420
+ const res = await sendToDaemon({ type, data: target === "all" ? undefined : { target } });
447
421
  if (!res.success) {
448
422
  console.error(colorize(`Error: ${res.error}`, "red"));
449
423
  process.exit(1);
@@ -454,9 +428,7 @@ async function cmdReload(args: string[]) {
454
428
  async function cmdDelete(args: string[]) {
455
429
  const target = args[0] || "all";
456
430
  const type = target === "all" ? "deleteAll" : "delete";
457
- const data = target === "all" ? undefined : { target };
458
-
459
- const res = await sendToDaemon({ type, data });
431
+ const res = await sendToDaemon({ type, data: target === "all" ? undefined : { target } });
460
432
  if (!res.success) {
461
433
  console.error(colorize(`Error: ${res.error}`, "red"));
462
434
  process.exit(1);
@@ -480,50 +452,21 @@ async function cmdDescribe(args: string[]) {
480
452
  console.error(colorize("Usage: bm2 describe <id|name>", "red"));
481
453
  process.exit(1);
482
454
  }
483
-
484
455
  const res = await sendToDaemon({ type: "describe", data: { target } });
485
456
  if (!res.success) {
486
457
  console.error(colorize(`Error: ${res.error}`, "red"));
487
458
  process.exit(1);
488
459
  }
489
-
490
460
  const processes: ProcessState[] = res.data;
491
461
  for (const p of processes) {
492
462
  console.log(colorize(`\n─── ${p.name} (id: ${p.pm_id}) ───`, "bold"));
493
463
  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
464
  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"}`);
465
+ console.log(` Log (out) : ${p.pm2_env.pm_out_log_path}`);
466
+ console.log(` Log (err) : ${p.pm2_env.pm_err_log_path}`);
502
467
  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
468
  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();
469
+ console.log(` CPU : ${p.monit.cpu.toFixed(1)}%`);
527
470
  }
528
471
  }
529
472
 
@@ -531,26 +474,17 @@ async function cmdLogs(args: string[]) {
531
474
  const target = args[0] || "all";
532
475
  let lines = 20;
533
476
  const linesIdx = args.indexOf("--lines");
534
- if (linesIdx !== -1 && args[linesIdx + 1]) {
535
- lines = parseInt(args[linesIdx + 1]!);
536
- }
537
-
477
+ if (linesIdx !== -1 && args[linesIdx + 1]) lines = parseInt(args[linesIdx + 1]!);
478
+
538
479
  const res = await sendToDaemon({ type: "logs", data: { target, lines } });
539
480
  if (!res.success) {
540
481
  console.error(colorize(`Error: ${res.error}`, "red"));
541
482
  process.exit(1);
542
483
  }
543
-
544
484
  for (const log of res.data) {
545
485
  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
- }
486
+ if (log.out) console.log(log.out);
487
+ if (log.err) console.log(colorize(log.err, "red"));
554
488
  }
555
489
  }
556
490
 
@@ -571,7 +505,6 @@ async function cmdScale(args: string[]) {
571
505
  console.error(colorize("Usage: bm2 scale <name|id> <count>", "red"));
572
506
  process.exit(1);
573
507
  }
574
-
575
508
  const res = await sendToDaemon({ type: "scale", data: { target, count } });
576
509
  if (!res.success) {
577
510
  console.error(colorize(`Error: ${res.error}`, "red"));
@@ -598,31 +531,13 @@ async function cmdResurrect() {
598
531
  printProcessTable(res.data);
599
532
  }
600
533
 
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);
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"));
626
541
  }
627
542
 
628
543
  async function cmdMonit() {
@@ -631,343 +546,16 @@ async function cmdMonit() {
631
546
  console.error(colorize(`Error: ${res.error}`, "red"));
632
547
  process.exit(1);
633
548
  }
634
-
635
549
  const snapshot = res.data;
636
550
  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"));
551
+ console.log(`System: CPU ${snapshot.system.cpuCount} cores | Load ${snapshot.system.loadAvg[0]?.toFixed(2)}`);
646
552
  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);
553
+ console.log(`${padRight(p.name, 20)} ${colorize(p.status, statusColor(p.status))} CPU: ${p.cpu.toFixed(1)}% Mem: ${formatBytes(p.memory)}`);
877
554
  }
878
- console.log(res.data);
879
555
  }
880
556
 
881
557
  // ---------------------------------------------------------------------------
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
558
+ // Main Dispatch
971
559
  // ---------------------------------------------------------------------------
972
560
 
973
561
  const args = process.argv.slice(2);
@@ -975,103 +563,25 @@ const command = args[0];
975
563
  const commandArgs = args.slice(1);
976
564
 
977
565
  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;
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;
1067
580
  case "help":
1068
- case "-h":
1069
- case "--help":
1070
581
  case undefined:
1071
- printHelp();
582
+ console.log(`BM2 v${VERSION} - Usage: bm2 <command> [options]`);
1072
583
  break;
1073
584
  default:
1074
585
  console.error(colorize(`Unknown command: ${command}`, "red"));
1075
- console.error(`Run ${colorize("bm2 --help", "cyan")} for usage information.`);
1076
586
  process.exit(1);
1077
587
  }