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