bm2 1.0.34 → 1.0.36

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