bm2 1.0.35 → 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,890 +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
+ class BM2CLI {
55
56
 
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;
64
- }
65
- }
57
+ // -------------------------------------------------------------------------
58
+ // Daemon helpers
59
+ // -------------------------------------------------------------------------
66
60
 
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, "");
78
-
79
- const child = Bun.spawn([bunPath, "run", daemonScript], {
80
- stdout,
81
- stderr,
82
- stdin: "ignore",
83
- });
84
-
85
- // Detach so the daemon outlives the CLI
86
- child.unref();
87
-
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)");
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
+ }
100
70
  }
101
- }
102
71
 
103
- async function stopDaemon(): Promise<void> {
104
- try {
105
-
106
- if (!isDaemonRunning()) return;
72
+ async startDaemon(): Promise<void> {
107
73
 
108
- const pidText = await Bun.file(DAEMON_PID_FILE).text();
109
- const pid = Number(pidText);
74
+ if (this.isDaemonRunning()) return;
110
75
 
111
- process.kill(pid, "SIGTERM"); // graceful stop
76
+ const daemonScript = join(import.meta.dir, "daemon.ts");
77
+ const bunPath = Bun.which("bun") || "bun";
112
78
 
113
- console.error("Daemon stopped");
79
+ const stdout = Bun.file(DAEMON_OUT_LOG_FILE);
80
+ const stderr = Bun.file(DAEMON_ERR_LOG_FILE);
114
81
 
115
- // cleanup
116
- await Bun.write(DAEMON_PID_FILE, "");
117
- } catch (err) {
118
- console.error("Failed to stop daemon:", err);
119
- }
120
- }
82
+ if (!(await stdout.exists())) await Bun.write(stdout, "");
83
+ if (!(await stderr.exists())) await Bun.write(stderr, "");
121
84
 
122
- async function sendToDaemon(msg: DaemonMessage): Promise<DaemonResponse> {
123
-
124
- //start the daemon
125
- await startDaemon();
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),
85
+ const child = Bun.spawn([bunPath, "run", daemonScript], {
86
+ stdout,
87
+ stderr,
88
+ stdin: "ignore",
140
89
  });
141
-
142
- if (!res.ok) {
143
- 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)");
144
103
  }
145
-
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
104
  }
156
-
157
- }
158
105
 
106
+ async stopDaemon(): Promise<void> {
107
+ try {
108
+ if (!this.isDaemonRunning()) return;
159
109
 
160
- // ---------------------------------------------------------------------------
161
- // Ecosystem config loader
162
- // ---------------------------------------------------------------------------
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
+ });
134
+
135
+ if (!res.ok) {
136
+ throw new Error(`Daemon error: ${res.status}`);
137
+ }
163
138
 
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}`);
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
+ }
172
146
  }
173
147
 
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;
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
+ }
184
157
  }
185
-
186
- const cwd = path.dirname(abs);
158
+
159
+ // -------------------------------------------------------------------------
160
+ // Ecosystem config loader
161
+ // -------------------------------------------------------------------------
162
+
163
+ async loadEcosystemConfig(filePath: string): Promise<EcosystemConfig> {
187
164
 
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
-
195
- return config
196
- }
165
+ const abs = resolve(filePath);
166
+ const file = Bun.file(abs);
197
167
 
198
- // ---------------------------------------------------------------------------
199
- // Parse CLI flags into StartOptions
200
- // ---------------------------------------------------------------------------
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;
174
+
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
+ }
201
181
 
202
- function parseStartFlags(args: string[], scriptOrConfig: string): StartOptions {
203
- const opts: StartOptions = { script: scriptOrConfig };
182
+ const cwd = path.dirname(abs);
204
183
 
205
- let i = 0;
206
- const positionalArgs: string[] = [];
184
+ config.apps = config.apps.map((i) => {
185
+ if ((i.cwd || "").trim() === "") i.cwd = cwd;
186
+ return i;
187
+ });
207
188
 
208
- while (i < args.length) {
209
- const arg = args[i]!;
189
+ return config;
190
+ }
210
191
 
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;
277
- }
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
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 === "--") {
332
216
  positionalArgs.push(...args.slice(i + 1));
333
- i = args.length;
334
217
  break;
335
- default:
336
- if (!arg.startsWith("-")) {
337
- 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;
338
290
  }
339
- 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++;
340
360
  }
341
- i++;
342
- }
343
361
 
344
- if (positionalArgs.length > 0) {
345
- opts.args = positionalArgs;
362
+ if (positionalArgs.length > 0) opts.args = positionalArgs;
363
+
364
+ return opts;
346
365
  }
347
366
 
348
- return opts;
349
- }
367
+ // -------------------------------------------------------------------------
368
+ // Commands
369
+ // -------------------------------------------------------------------------
350
370
 
351
- // ---------------------------------------------------------------------------
352
- // Commands
353
- // ---------------------------------------------------------------------------
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
+ }
354
376
 
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
- }
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 });
363
420
 
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
421
  if (!res.success) {
376
422
  console.error(colorize(`Error: ${res.error}`, "red"));
377
423
  process.exit(1);
378
424
  }
425
+
379
426
  printProcessTable(res.data);
380
- return;
381
- }
382
427
 
383
- // Single script
384
- const opts = parseStartFlags(args.slice(1), resolve(scriptOrConfig));
385
- opts.script = resolve(scriptOrConfig);
386
-
387
- const cwd = path.dirname(opts.script);
388
-
389
- opts.cwd = cwd;
390
-
391
- const res = await sendToDaemon({ type: "start", data: opts });
392
-
393
- if (!res.success) {
394
- console.error(colorize(`Error: ${res.error}`, "red"));
395
- process.exit(1);
428
+ if (noDaemon) {
429
+ await new Promise(() => {});
430
+ }
396
431
  }
397
- printProcessTable(res.data);
398
- }
399
432
 
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 };
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 };
404
437
 
405
- const res = await sendToDaemon({ type, data });
406
- if (!res.success) {
407
- console.error(colorize(`Error: ${res.error}`, "red"));
408
- 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);
409
444
  }
410
- printProcessTable(res.data);
411
- }
412
445
 
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 };
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 };
417
450
 
418
- const res = await sendToDaemon({ type, data });
419
- if (!res.success) {
420
- console.error(colorize(`Error: ${res.error}`, "red"));
421
- 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);
422
457
  }
423
- printProcessTable(res.data);
424
- }
425
458
 
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 };
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 };
430
463
 
431
- const res = await sendToDaemon({ type, data });
432
- if (!res.success) {
433
- console.error(colorize(`Error: ${res.error}`, "red"));
434
- 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);
435
470
  }
436
- printProcessTable(res.data);
437
- }
438
471
 
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 };
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 };
443
476
 
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
- }
453
-
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:
477
+ const res = await this.sendToDaemon({ type, data });
478
+ if (!res.success) {
479
+ console.error(colorize(`Error: ${res.error}`, "red"));
480
+ process?.exit(1);
469
481
  }
470
- }
471
-
472
- if (liveMode) {
473
- liveWatchProcess(res.data)
474
- } else {
482
+
483
+ console.log(colorize("✓ Deleted", "green"));
475
484
  printProcessTable(res.data);
476
485
  }
477
- }
478
486
 
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
- }
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
+ }
485
493
 
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
- }
494
+ const liveMode = args.includes("--live");
491
495
 
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();
496
+ if (liveMode) {
497
+ liveWatchProcess(res.data);
498
+ } else {
499
+ printProcessTable(res.data);
500
+ }
529
501
  }
530
- }
531
502
 
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
- }
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
+ }
509
+
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
+ }
539
515
 
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);
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
+ }
544
550
  }
545
551
 
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);
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]!);
551
558
  }
552
- if (log.err) {
553
- console.log(colorize("--- stderr ---", "red"));
554
- 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);
555
564
  }
556
- }
557
- }
558
565
 
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);
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
+ }
565
577
  }
566
- console.log(colorize("✓ Logs flushed", "green"));
567
- }
568
578
 
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);
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"));
575
587
  }
576
588
 
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
- }
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
+ }
584
596
 
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);
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);
590
603
  }
591
- console.log(colorize("✓ Process list saved", "green"));
592
- }
593
604
 
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);
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"));
599
612
  }
600
- printProcessTable(res.data);
601
- }
602
613
 
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);
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);
609
621
  }
610
622
 
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
- }
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
+ }
618
630
 
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);
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"));
625
637
  }
626
- console.log(colorize("✓ Restart counters reset", "green"));
627
- printProcessTable(res.data);
628
- }
629
638
 
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);
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);
635
648
  }
636
649
 
637
- const snapshot = res.data;
638
- 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
+ }
639
656
 
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();
657
+ const snapshot = res.data;
658
+ console.log(colorize("\n⚡ BM2 Monitor\n", "bold"));
646
659
 
647
- console.log(colorize("Processes:", "cyan"));
648
- for (const p of snapshot.processes) {
649
- 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}`);
650
663
  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}`
664
+ ` Memory : ${formatBytes(
665
+ snapshot.system.totalMemory - snapshot.system.freeMemory
666
+ )} / ${formatBytes(snapshot.system.totalMemory)}`
652
667
  );
653
- }
654
- console.log();
655
- }
668
+ console.log(` Load avg : ${snapshot.system.loadAvg.map((l: number) => l.toFixed(2)).join(", ")}`);
669
+ console.log();
656
670
 
657
- async function cmdDashboard(args: string[]) {
658
- let port = DASHBOARD_PORT;
659
- 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
+ }
660
683
 
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]!);
684
+ async cmdDashboard(args: string[]) {
685
+ let port = DASHBOARD_PORT;
686
+ let metricsPort = METRICS_PORT;
665
687
 
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
- }
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]!);
674
692
 
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);
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
+ );
680
702
  }
681
- console.log(colorize("✓ Dashboard stopped", "green"));
682
- }
683
703
 
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"));
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);
693
709
  }
694
- } catch {
695
- console.log(colorize("✗ Daemon is not running", "red"));
710
+ console.log(colorize("✓ Dashboard stopped", "green"));
696
711
  }
697
- }
698
712
 
699
- async function cmdKill() {
700
- try {
701
- await sendToDaemon({ type: "kill" });
702
- } catch {
703
- // 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
+ }
704
726
  }
705
727
 
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
- }
728
+ async cmdKill() {
729
+ try {
730
+ await this.sendToDaemon({ type: "kill" });
731
+ } catch {
732
+ // Expected — daemon exits
733
+ }
716
734
 
717
- async function cmdDeploy(args: string[]) {
718
- const configFile = args[0];
719
- 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 {}
720
741
 
721
- if (!configFile || !environment) {
722
- console.error(colorize("Usage: bm2 deploy <config> <environment> [setup]", "red"));
723
- process.exit(1);
742
+ console.log(colorize("✓ Daemon killed", "green"));
724
743
  }
725
744
 
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
- }
745
+ async cmdDeploy(args: string[]) {
746
+ const configFile = args[0];
747
+ const environment = args[1];
732
748
 
733
- const deployConfig = config.deploy[environment]!;
734
- const deployer = new DeployManager();
749
+ if (!configFile || !environment) {
750
+ console.error(colorize("Usage: bm2 deploy <config> <environment> [setup]", "red"));
751
+ process.exit(1);
752
+ }
735
753
 
736
- if (args[2] === "setup") {
737
- await deployer.setup(deployConfig);
738
- } else {
739
- await deployer.deploy(deployConfig, args[2]);
740
- }
741
- }
754
+ const config = await this.loadEcosystemConfig(configFile);
742
755
 
743
- async function cmdStartup(args: string[]) {
744
- 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
+ }
745
760
 
746
- if (args[0] === "remove" || args[0] === "uninstall") {
747
- const result = await startup.uninstall();
748
- console.log(result);
749
- return;
750
- }
761
+ const deployConfig = config.deploy[environment]!;
762
+ const deployer = new DeployManager();
751
763
 
752
- if (args[0] === "install") {
753
- const result = await startup.install();
754
- console.log(result);
755
- return;
764
+ if (args[2] === "setup") {
765
+ await deployer.setup(deployConfig);
766
+ } else {
767
+ await deployer.deploy(deployConfig, args[2]);
768
+ }
756
769
  }
757
770
 
758
- // Just print the config
759
- const content = await startup.generate(args[0]);
760
- console.log(content);
761
- }
771
+ async cmdStartup(args: string[]) {
772
+ const startup = new StartupManager();
762
773
 
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;
774
+ if (args[0] === "remove" || args[0] === "uninstall") {
775
+ console.log(await startup.uninstall());
776
+ return;
779
777
  }
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;
778
+
779
+ if (args[0] === "install") {
780
+ console.log(await startup.install());
781
+ return;
791
782
  }
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);
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;
799
803
  }
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"));
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);
808
811
  for (const [k, v] of Object.entries(env)) {
809
- console.log(` ${colorize(k, "cyan")}=${v}`);
812
+ console.log(`${colorize(k, "cyan")}=${v}`);
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);
810
823
  }
824
+ await envMgr.deleteEnv(name, key);
825
+ console.log(colorize("✓ Deleted", "green"));
826
+ break;
827
+ }
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;
811
837
  }
812
- break;
838
+ default:
839
+ console.error(colorize("Usage: bm2 env <set|get|delete|list> ...", "red"));
840
+ process.exit(1);
813
841
  }
814
- default:
815
- console.error(colorize("Usage: bm2 env <set|get|delete|list> ...", "red"));
816
- process.exit(1);
817
842
  }
818
- }
819
843
 
820
- async function cmdModule(args: string[]) {
821
- const subCmd = args[0];
844
+ async cmdModule(args: string[]) {
845
+ const subCmd = args[0];
822
846
 
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);
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;
829
861
  }
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);
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;
834
876
  }
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);
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;
844
892
  }
845
- const res = await sendToDaemon({ type: "moduleUninstall", data: { module: mod } });
846
- if (!res.success) {
847
- console.error(colorize(`Error: ${res.error}`, "red"));
893
+ default:
894
+ console.error(colorize("Usage: bm2 module <install|uninstall|list> ...", "red"));
848
895
  process.exit(1);
849
- }
850
- console.log(colorize("✓ Module uninstalled", "green"));
851
- break;
852
896
  }
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"));
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"));
862
905
  } else {
863
- for (const m of res.data) {
864
- console.log(` ${colorize(m.name, "cyan")} @ ${m.version}`);
865
- }
906
+ console.error(colorize("stopped", "red"));
866
907
  }
867
- break;
868
- }
869
- default:
870
- console.error(colorize("Usage: bm2 module <install|uninstall|list> ...", "red"));
871
908
  process.exit(1);
872
- }
873
- }
909
+ };
874
910
 
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"));
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);
883
931
  }
884
-
885
- process.exit(1);
886
932
  }
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
- }
911
-
912
- }
913
933
 
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);
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);
919
941
  }
920
- console.log(res.data);
921
- }
922
942
 
923
- // ---------------------------------------------------------------------------
924
- // Help
925
- // ---------------------------------------------------------------------------
943
+ // -------------------------------------------------------------------------
944
+ // Help
945
+ // -------------------------------------------------------------------------
926
946
 
927
- function printHelp() {
947
+ printHelp() {
928
948
  console.log(`
929
949
  ${colorize("BM2", "bold")} ${colorize(`v${VERSION}`, "dim")} — Bun Process Manager
930
950
 
@@ -973,10 +993,10 @@ function printHelp() {
973
993
  ${colorize("Daemon:", "cyan")}
974
994
  daemon status Returns the status of the daemon
975
995
  daemon start Starts the daemon
976
- daemon start Stops the daemon
996
+ daemon stop Stops the daemon
977
997
  daemon reload Reloads the daemon
978
998
 
979
- ${colorize("Daemon:", "cyan")}
999
+ ${colorize("Other:", "cyan")}
980
1000
  ping Check if daemon is alive
981
1001
  kill Kill the daemon and all processes
982
1002
  sendSignal <sig> <id|name> Send OS signal to process
@@ -984,7 +1004,7 @@ function printHelp() {
984
1004
  ${colorize("Start Options:", "dim")}
985
1005
  --name, -n <name> Process name
986
1006
  --instances, -i <N> Number of instances (cluster)
987
- --exec-mode, -x <mode> fork or cluster
1007
+ --exec-mode, -x <mode> fork or cluster
988
1008
  --watch, -w Watch for file changes
989
1009
  --cwd <path> Working directory
990
1010
  --interpreter <bin> Custom interpreter
@@ -995,6 +1015,7 @@ function printHelp() {
995
1015
  --port, -p <port> Base port for cluster
996
1016
  --env <KEY=VALUE> Set environment variable
997
1017
  --no-autorestart Disable auto-restart
1018
+ --no-daemon, -d Run without daemon (blocks)
998
1019
  --log, -o <file> Custom stdout log path
999
1020
  --error, -e <file> Custom stderr log path
1000
1021
  --namespace <ns> Namespace grouping
@@ -1005,6 +1026,8 @@ function printHelp() {
1005
1026
  ${colorize("Examples:", "dim")}
1006
1027
  bm2 start app.ts
1007
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
1008
1031
  bm2 start ecosystem.config.ts
1009
1032
  bm2 restart api
1010
1033
  bm2 scale api 8
@@ -1012,117 +1035,126 @@ function printHelp() {
1012
1035
  bm2 monit
1013
1036
  bm2 save && bm2 resurrect
1014
1037
  `);
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);
1024
-
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
+
1025
1048
  switch (command) {
1026
- case "start":
1027
- await cmdStart(commandArgs);
1049
+ case "start":
1050
+ await this.cmdStart(commandArgs);
1028
1051
  break;
1029
- case "stop":
1030
- await cmdStop(commandArgs);
1052
+ case "stop":
1053
+ await this.cmdStop(commandArgs);
1031
1054
  break;
1032
- case "restart":
1033
- await cmdRestart(commandArgs);
1055
+ case "restart":
1056
+ await this.cmdRestart(commandArgs);
1034
1057
  break;
1035
- case "reload":
1036
- await cmdReload(commandArgs);
1058
+ case "reload":
1059
+ await this.cmdReload(commandArgs);
1037
1060
  break;
1038
- case "delete":
1039
- case "del":
1040
- case "rm":
1041
- await cmdDelete(commandArgs);
1061
+ case "delete":
1062
+ case "del":
1063
+ case "rm":
1064
+ await this.cmdDelete(commandArgs);
1042
1065
  break;
1043
- case "scale":
1044
- await cmdScale(commandArgs);
1066
+ case "scale":
1067
+ await this.cmdScale(commandArgs);
1045
1068
  break;
1046
- case "list":
1047
- case "ls":
1048
- case "status":
1049
- await cmdList(commandArgs);
1069
+ case "list":
1070
+ case "ls":
1071
+ case "status":
1072
+ await this.cmdList(commandArgs);
1050
1073
  break;
1051
- case "describe":
1052
- case "show":
1053
- case "info":
1054
- await cmdDescribe(commandArgs);
1074
+ case "describe":
1075
+ case "show":
1076
+ case "info":
1077
+ await this.cmdDescribe(commandArgs);
1055
1078
  break;
1056
- case "logs":
1057
- case "log":
1058
- await cmdLogs(commandArgs);
1079
+ case "logs":
1080
+ case "log":
1081
+ await this.cmdLogs(commandArgs);
1059
1082
  break;
1060
- case "flush":
1061
- await cmdFlush(commandArgs);
1083
+ case "flush":
1084
+ await this.cmdFlush(commandArgs);
1062
1085
  break;
1063
- case "monit":
1064
- case "monitor":
1065
- await cmdMonit();
1086
+ case "monit":
1087
+ case "monitor":
1088
+ await this.cmdMonit();
1066
1089
  break;
1067
- case "dashboard":
1090
+ case "dashboard":
1068
1091
  if (commandArgs[0] === "stop") {
1069
- await cmdDashboardStop();
1092
+ await this.cmdDashboardStop();
1070
1093
  } else {
1071
- await cmdDashboard(commandArgs);
1094
+ await this.cmdDashboard(commandArgs);
1072
1095
  }
1073
1096
  break;
1074
- case "prometheus":
1075
- await cmdPrometheus();
1097
+ case "prometheus":
1098
+ await this.cmdPrometheus();
1076
1099
  break;
1077
- case "save":
1078
- case "dump":
1079
- await cmdSave();
1100
+ case "save":
1101
+ case "dump":
1102
+ await this.cmdSave();
1080
1103
  break;
1081
- case "resurrect":
1082
- case "restore":
1083
- await cmdResurrect();
1104
+ case "resurrect":
1105
+ case "restore":
1106
+ await this.cmdResurrect();
1084
1107
  break;
1085
- case "reset":
1086
- await cmdReset(commandArgs);
1108
+ case "reset":
1109
+ await this.cmdReset(commandArgs);
1087
1110
  break;
1088
- case "sendSignal":
1089
- case "signal":
1090
- await cmdSignal(commandArgs);
1111
+ case "sendSignal":
1112
+ case "signal":
1113
+ await this.cmdSignal(commandArgs);
1091
1114
  break;
1092
- case "ping":
1093
- await cmdPing();
1115
+ case "ping":
1116
+ await this.cmdPing();
1094
1117
  break;
1095
- case "kill":
1096
- await cmdKill();
1118
+ case "kill":
1119
+ await this.cmdKill();
1097
1120
  break;
1098
- case "deploy":
1099
- await cmdDeploy(commandArgs);
1121
+ case "deploy":
1122
+ await this.cmdDeploy(commandArgs);
1100
1123
  break;
1101
- case "startup":
1102
- await cmdStartup(commandArgs);
1124
+ case "startup":
1125
+ await this.cmdStartup(commandArgs);
1103
1126
  break;
1104
- case "env":
1105
- await cmdEnv(commandArgs);
1127
+ case "env":
1128
+ await this.cmdEnv(commandArgs);
1106
1129
  break;
1107
- case "module":
1108
- await cmdModule(commandArgs);
1130
+ case "module":
1131
+ await this.cmdModule(commandArgs);
1109
1132
  break;
1110
- case "daemon":
1111
- await cmdDaemon(commandArgs);
1133
+ case "daemon":
1134
+ await this.cmdDaemon(commandArgs);
1112
1135
  break;
1113
- case "version":
1114
- case "-v":
1115
- case "--version":
1136
+ case "version":
1137
+ case "-v":
1138
+ case "--version":
1116
1139
  console.log(`${APP_NAME} v${VERSION}`);
1117
1140
  break;
1118
- case "help":
1119
- case "-h":
1120
- case "--help":
1121
- case undefined:
1122
- printHelp();
1141
+ case "help":
1142
+ case "-h":
1143
+ case "--help":
1144
+ case undefined:
1145
+ this.printHelp();
1123
1146
  break;
1124
- default:
1147
+ default:
1125
1148
  console.error(colorize(`Unknown command: ${command}`, "red"));
1126
1149
  console.error(`Run ${colorize("bm2 --help", "cyan")} for usage information.`);
1127
1150
  process.exit(1);
1151
+ }
1152
+ }
1128
1153
  }
1154
+
1155
+ // ---------------------------------------------------------------------------
1156
+ // Entrypoint
1157
+ // ---------------------------------------------------------------------------
1158
+
1159
+ const cli = new BM2CLI();
1160
+ await cli.run(process.argv.slice(2));