bm2 1.0.35 → 1.0.37

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