bm2 1.0.0 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -2
- package/src/hello.js +2 -0
- package/src/index.ts +583 -11
- /package/src/{startup.ts → startup-manager.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bm2",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "A blazing-fast, full-featured process manager built entirely on Bun native APIs. The modern PM2 replacement — zero Node.js dependencies, pure Bun performance.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"module": "src/index.ts",
|
|
@@ -59,9 +59,12 @@
|
|
|
59
59
|
"linux",
|
|
60
60
|
"darwin"
|
|
61
61
|
],
|
|
62
|
-
"dependencies": {
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"ws": "^8.19.0"
|
|
64
|
+
},
|
|
63
65
|
"devDependencies": {
|
|
64
66
|
"@types/bun": "^1.3.9",
|
|
67
|
+
"@types/ws": "^8.18.1",
|
|
65
68
|
"bun-types": "latest",
|
|
66
69
|
"typescript": "^5.9.3"
|
|
67
70
|
}
|
package/src/hello.js
ADDED
package/src/index.ts
CHANGED
|
@@ -1,15 +1,587 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
1
2
|
/**
|
|
2
3
|
* BM2 — Bun Process Manager
|
|
3
4
|
* A production-grade process manager for Bun.
|
|
4
|
-
*
|
|
5
|
-
* Features:
|
|
6
|
-
* - Fork & cluster execution modes
|
|
7
|
-
* - Auto-restart & crash recovery
|
|
8
|
-
* - Health checks & monitoring
|
|
9
|
-
* - Log management & rotation
|
|
10
|
-
* - Deployment support
|
|
11
|
-
*
|
|
12
|
-
* https://github.com/your-org/bm2
|
|
13
|
-
* License: GPL-3.0-only
|
|
14
|
-
* Author: Zak <zak@maxxpainn.com>
|
|
15
5
|
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync, unlinkSync, openSync } from "fs";
|
|
8
|
+
import { resolve, join, extname } from "path";
|
|
9
|
+
import { createConnection } from "net";
|
|
10
|
+
import {
|
|
11
|
+
APP_NAME,
|
|
12
|
+
VERSION,
|
|
13
|
+
DAEMON_SOCKET,
|
|
14
|
+
DAEMON_PID_FILE,
|
|
15
|
+
BM2_HOME,
|
|
16
|
+
DASHBOARD_PORT,
|
|
17
|
+
METRICS_PORT,
|
|
18
|
+
} from "./constants";
|
|
19
|
+
import { ensureDirs, formatBytes, formatUptime, colorize, padRight } from "./utils";
|
|
20
|
+
import { DeployManager } from "./deploy";
|
|
21
|
+
import { StartupManager } from "./startup-manager";
|
|
22
|
+
import { EnvManager } from "./env-manager";
|
|
23
|
+
import type {
|
|
24
|
+
StartOptions,
|
|
25
|
+
EcosystemConfig,
|
|
26
|
+
ProcessState,
|
|
27
|
+
} from "./types";
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Ensure directory structure exists
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
ensureDirs();
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Helpers
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Reads the last N lines from the daemon error log to help debug crashes.
|
|
40
|
+
*/
|
|
41
|
+
function getDaemonErrorLog(linesToRead = 10): string {
|
|
42
|
+
const logPath = join(BM2_HOME, "daemon.err.log");
|
|
43
|
+
if (!existsSync(logPath)) return "";
|
|
44
|
+
try {
|
|
45
|
+
const content = readFileSync(logPath, "utf-8").trim();
|
|
46
|
+
if (!content) return "";
|
|
47
|
+
const lines = content.split("\n");
|
|
48
|
+
return lines.slice(-linesToRead).join("\n");
|
|
49
|
+
} catch {
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if the daemon process is actually running by reading the PID file
|
|
56
|
+
* and sending signal 0 to verify the process exists.
|
|
57
|
+
*/
|
|
58
|
+
function isDaemonRunning(): boolean {
|
|
59
|
+
if (!existsSync(DAEMON_PID_FILE)) return false;
|
|
60
|
+
try {
|
|
61
|
+
const pid = parseInt(readFileSync(DAEMON_PID_FILE, "utf-8").trim());
|
|
62
|
+
if (isNaN(pid)) return false;
|
|
63
|
+
process.kill(pid, 0); // signal 0 — just check existence
|
|
64
|
+
return true;
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Remove stale socket and PID files left behind by a crashed daemon.
|
|
72
|
+
*/
|
|
73
|
+
function cleanupStaleFiles(): void {
|
|
74
|
+
try {
|
|
75
|
+
if (existsSync(DAEMON_SOCKET)) unlinkSync(DAEMON_SOCKET);
|
|
76
|
+
} catch { /* ignore */ }
|
|
77
|
+
try {
|
|
78
|
+
if (existsSync(DAEMON_PID_FILE)) unlinkSync(DAEMON_PID_FILE);
|
|
79
|
+
} catch { /* ignore */ }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Start the daemon process if it is not already running.
|
|
84
|
+
* Redirects daemon stdout/stderr to ~/.bm2/daemon.{out|err}.log
|
|
85
|
+
*/
|
|
86
|
+
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();
|
|
106
|
+
|
|
107
|
+
const daemonScript = join(import.meta.dir, "daemon.ts");
|
|
108
|
+
const bunPath = Bun.which("bun") || "bun";
|
|
109
|
+
|
|
110
|
+
if (!existsSync(daemonScript)) {
|
|
111
|
+
throw new Error(`Daemon script not found at: ${daemonScript}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Prepare log files for the daemon so we can see why it crashes
|
|
115
|
+
const outLog = join(BM2_HOME, "daemon.out.log");
|
|
116
|
+
const errLog = join(BM2_HOME, "daemon.err.log");
|
|
117
|
+
|
|
118
|
+
// Open file descriptors (append mode)
|
|
119
|
+
const outFd = openSync(outLog, "a");
|
|
120
|
+
const errFd = openSync(errLog, "a");
|
|
121
|
+
|
|
122
|
+
// Spawn detached
|
|
123
|
+
const child = Bun.spawn([bunPath, "run", daemonScript], {
|
|
124
|
+
stdin: "ignore",
|
|
125
|
+
stdout: outFd,
|
|
126
|
+
stderr: errFd,
|
|
127
|
+
detached: true,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
child.unref();
|
|
131
|
+
|
|
132
|
+
// Wait for socket
|
|
133
|
+
const maxRetries = 50; // 5 seconds
|
|
134
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
135
|
+
if (existsSync(DAEMON_SOCKET)) return;
|
|
136
|
+
await Bun.sleep(100);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// If we timed out, read the error log to show the user
|
|
140
|
+
const recentErrors = getDaemonErrorLog();
|
|
141
|
+
throw new Error(
|
|
142
|
+
"Daemon failed to start (socket not found).\n" +
|
|
143
|
+
(recentErrors
|
|
144
|
+
? `\n--- Daemon Stderr (last 10 lines) ---\n${recentErrors}\n-------------------------------------`
|
|
145
|
+
: `Check logs at: ${errLog}`)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Send a JSON message to the daemon and wait for a response.
|
|
151
|
+
*/
|
|
152
|
+
async function sendToDaemon(message: object): Promise<any> {
|
|
153
|
+
await startDaemon();
|
|
154
|
+
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
let settled = false;
|
|
157
|
+
|
|
158
|
+
function settle(fn: typeof resolve | typeof reject, value: any) {
|
|
159
|
+
if (settled) return;
|
|
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
|
+
}
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Table Rendering & Utilities
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
function statusColor(status: string): string {
|
|
238
|
+
switch (status) {
|
|
239
|
+
case "online": return "green";
|
|
240
|
+
case "stopped": return "gray";
|
|
241
|
+
case "errored": return "red";
|
|
242
|
+
case "launching":
|
|
243
|
+
case "waiting-restart": return "yellow";
|
|
244
|
+
case "stopping": return "magenta";
|
|
245
|
+
default: return "white";
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function printProcessTable(processes: ProcessState[]) {
|
|
250
|
+
if (!processes || processes.length === 0) {
|
|
251
|
+
console.log(colorize("No processes running", "dim"));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const header = [
|
|
256
|
+
padRight("id", 4),
|
|
257
|
+
padRight("name", 20),
|
|
258
|
+
padRight("namespace", 12),
|
|
259
|
+
padRight("ver", 8),
|
|
260
|
+
padRight("mode", 8),
|
|
261
|
+
padRight("pid", 8),
|
|
262
|
+
padRight("uptime", 10),
|
|
263
|
+
padRight("↺", 4),
|
|
264
|
+
padRight("status", 16),
|
|
265
|
+
padRight("cpu", 8),
|
|
266
|
+
padRight("mem", 10),
|
|
267
|
+
].join(" ");
|
|
268
|
+
|
|
269
|
+
console.log(colorize(header, "dim"));
|
|
270
|
+
console.log(colorize("─".repeat(header.length), "dim"));
|
|
271
|
+
|
|
272
|
+
for (const p of processes) {
|
|
273
|
+
const uptime = p.status === "online" ? formatUptime(Date.now() - p.pm2_env.pm_uptime) : "0s";
|
|
274
|
+
const row = [
|
|
275
|
+
padRight(String(p.pm_id), 4),
|
|
276
|
+
padRight(p.name, 20),
|
|
277
|
+
padRight(p.namespace || "default", 12),
|
|
278
|
+
padRight(p.pm2_env.version || "N/A", 8),
|
|
279
|
+
padRight(p.pm2_env.execMode, 8),
|
|
280
|
+
padRight(p.pid ? String(p.pid) : "N/A", 8),
|
|
281
|
+
padRight(uptime, 10),
|
|
282
|
+
padRight(String(p.pm2_env.restart_time), 4),
|
|
283
|
+
padRight(p.status, 16),
|
|
284
|
+
padRight(p.monit.cpu.toFixed(1) + "%", 8),
|
|
285
|
+
padRight(formatBytes(p.monit.memory), 10),
|
|
286
|
+
];
|
|
287
|
+
console.log(row.join(" ").replace(p.status, colorize(p.status, statusColor(p.status))));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function loadEcosystemConfig(filePath: string): Promise<EcosystemConfig> {
|
|
292
|
+
const abs = resolve(filePath);
|
|
293
|
+
if (!existsSync(abs)) throw new Error(`Ecosystem file not found: ${abs}`);
|
|
294
|
+
const ext = extname(abs);
|
|
295
|
+
if (ext === ".json") return await Bun.file(abs).json();
|
|
296
|
+
const mod = await import(abs);
|
|
297
|
+
return mod.default || mod;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function parseStartFlags(args: string[], scriptOrConfig: string): StartOptions {
|
|
301
|
+
const opts: StartOptions = { script: scriptOrConfig };
|
|
302
|
+
let i = 0;
|
|
303
|
+
const positionalArgs: string[] = [];
|
|
304
|
+
|
|
305
|
+
while (i < args.length) {
|
|
306
|
+
const arg = args[i]!;
|
|
307
|
+
switch (arg) {
|
|
308
|
+
case "--name": case "-n": opts.name = args[++i]; break;
|
|
309
|
+
case "--instances": case "-i": opts.instances = parseInt(args[++i]!) || 1; break;
|
|
310
|
+
case "--cwd": opts.cwd = args[++i]; break;
|
|
311
|
+
case "--interpreter": opts.interpreter = args[++i]; break;
|
|
312
|
+
case "--interpreter-args": opts.interpreterArgs = args[++i]!.split(" "); break;
|
|
313
|
+
case "--node-args": opts.nodeArgs = args[++i]!.split(" "); break;
|
|
314
|
+
case "--watch": case "-w": opts.watch = true; break;
|
|
315
|
+
case "--watch-path":
|
|
316
|
+
if (!Array.isArray(opts.watch)) opts.watch = [];
|
|
317
|
+
(opts.watch as string[]).push(args[++i]!);
|
|
318
|
+
break;
|
|
319
|
+
case "--ignore-watch": opts.ignoreWatch = args[++i]!.split(","); break;
|
|
320
|
+
case "--exec-mode": case "-x": opts.execMode = args[++i] as "fork" | "cluster"; break;
|
|
321
|
+
case "--max-memory-restart": opts.maxMemoryRestart = args[++i]; break;
|
|
322
|
+
case "--max-restarts": opts.maxRestarts = parseInt(args[++i]!); break;
|
|
323
|
+
case "--min-uptime": opts.minUptime = parseInt(args[++i]!); break;
|
|
324
|
+
case "--kill-timeout": opts.killTimeout = parseInt(args[++i]!); break;
|
|
325
|
+
case "--restart-delay": opts.restartDelay = parseInt(args[++i]!); break;
|
|
326
|
+
case "--cron": case "--cron-restart": opts.cron = args[++i]; break;
|
|
327
|
+
case "--no-autorestart": opts.autorestart = false; break;
|
|
328
|
+
case "--env": {
|
|
329
|
+
const p = args[++i]!;
|
|
330
|
+
const idx = p.indexOf("=");
|
|
331
|
+
if (idx !== -1) {
|
|
332
|
+
if (!opts.env) opts.env = {};
|
|
333
|
+
opts.env[p.substring(0, idx)] = p.substring(idx + 1);
|
|
334
|
+
}
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
case "--log": case "--output": case "-o": opts.outFile = args[++i]; break;
|
|
338
|
+
case "--error": case "-e": opts.errorFile = args[++i]; break;
|
|
339
|
+
case "--merge-logs": opts.mergeLogs = true; break;
|
|
340
|
+
case "--log-date-format": opts.logDateFormat = args[++i]; break;
|
|
341
|
+
case "--log-max-size": opts.logMaxSize = args[++i]; break;
|
|
342
|
+
case "--log-retain": opts.logRetain = parseInt(args[++i]!); break;
|
|
343
|
+
case "--log-compress": opts.logCompress = true; break;
|
|
344
|
+
case "--port": case "-p": opts.port = parseInt(args[++i]!); break;
|
|
345
|
+
case "--namespace": opts.namespace = args[++i]; break;
|
|
346
|
+
case "--": positionalArgs.push(...args.slice(i + 1)); i = args.length; break;
|
|
347
|
+
default: if (!arg.startsWith("-")) positionalArgs.push(arg); break;
|
|
348
|
+
}
|
|
349
|
+
i++;
|
|
350
|
+
}
|
|
351
|
+
if (positionalArgs.length > 0) opts.args = positionalArgs;
|
|
352
|
+
return opts;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
// Commands
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
async function cmdStart(args: string[]) {
|
|
360
|
+
const scriptOrConfig = args[0];
|
|
361
|
+
if (!scriptOrConfig) {
|
|
362
|
+
console.error(colorize("Usage: bm2 start <script|config> [options]", "red"));
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
|
|
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
|
+
const ext = extname(scriptOrConfig);
|
|
373
|
+
if (ext === ".json" || scriptOrConfig.includes("ecosystem") || scriptOrConfig.includes("bm2.config")) {
|
|
374
|
+
const config = await loadEcosystemConfig(scriptOrConfig);
|
|
375
|
+
const res = await sendToDaemon({ type: "ecosystem", data: config });
|
|
376
|
+
if (!res.success) {
|
|
377
|
+
console.error(colorize(`Error: ${res.error}`, "red"));
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
printProcessTable(res.data);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const opts = parseStartFlags(args.slice(1), resolve(scriptOrConfig));
|
|
385
|
+
opts.script = resolve(scriptOrConfig);
|
|
386
|
+
|
|
387
|
+
const res = await sendToDaemon({ type: "start", data: opts });
|
|
388
|
+
if (!res.success) {
|
|
389
|
+
console.error(colorize(`Error: ${res.error}`, "red"));
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
printProcessTable(res.data);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function cmdStop(args: string[]) {
|
|
396
|
+
const target = args[0] || "all";
|
|
397
|
+
const type = target === "all" ? "stopAll" : "stop";
|
|
398
|
+
const res = await sendToDaemon({ type, data: target === "all" ? undefined : { target } });
|
|
399
|
+
if (!res.success) {
|
|
400
|
+
console.error(colorize(`Error: ${res.error}`, "red"));
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
printProcessTable(res.data);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function cmdRestart(args: string[]) {
|
|
407
|
+
const target = args[0] || "all";
|
|
408
|
+
const type = target === "all" ? "restartAll" : "restart";
|
|
409
|
+
const res = await sendToDaemon({ type, data: target === "all" ? undefined : { target } });
|
|
410
|
+
if (!res.success) {
|
|
411
|
+
console.error(colorize(`Error: ${res.error}`, "red"));
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
printProcessTable(res.data);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function cmdReload(args: string[]) {
|
|
418
|
+
const target = args[0] || "all";
|
|
419
|
+
const type = target === "all" ? "reloadAll" : "reload";
|
|
420
|
+
const res = await sendToDaemon({ type, data: target === "all" ? undefined : { target } });
|
|
421
|
+
if (!res.success) {
|
|
422
|
+
console.error(colorize(`Error: ${res.error}`, "red"));
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
printProcessTable(res.data);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function cmdDelete(args: string[]) {
|
|
429
|
+
const target = args[0] || "all";
|
|
430
|
+
const type = target === "all" ? "deleteAll" : "delete";
|
|
431
|
+
const res = await sendToDaemon({ type, data: target === "all" ? undefined : { target } });
|
|
432
|
+
if (!res.success) {
|
|
433
|
+
console.error(colorize(`Error: ${res.error}`, "red"));
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
console.log(colorize("✓ Deleted", "green"));
|
|
437
|
+
printProcessTable(res.data);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function cmdList() {
|
|
441
|
+
const res = await sendToDaemon({ type: "list" });
|
|
442
|
+
if (!res.success) {
|
|
443
|
+
console.error(colorize(`Error: ${res.error}`, "red"));
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
printProcessTable(res.data);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function cmdDescribe(args: string[]) {
|
|
450
|
+
const target = args[0];
|
|
451
|
+
if (!target) {
|
|
452
|
+
console.error(colorize("Usage: bm2 describe <id|name>", "red"));
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
const res = await sendToDaemon({ type: "describe", data: { target } });
|
|
456
|
+
if (!res.success) {
|
|
457
|
+
console.error(colorize(`Error: ${res.error}`, "red"));
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
const processes: ProcessState[] = res.data;
|
|
461
|
+
for (const p of processes) {
|
|
462
|
+
console.log(colorize(`\n─── ${p.name} (id: ${p.pm_id}) ───`, "bold"));
|
|
463
|
+
console.log(` Status : ${colorize(p.status, statusColor(p.status))}`);
|
|
464
|
+
console.log(` Script : ${p.pm2_env.script}`);
|
|
465
|
+
console.log(` Log (out) : ${p.pm2_env.pm_out_log_path}`);
|
|
466
|
+
console.log(` Log (err) : ${p.pm2_env.pm_err_log_path}`);
|
|
467
|
+
console.log(` Restarts : ${p.pm2_env.restart_time}`);
|
|
468
|
+
console.log(` Memory : ${formatBytes(p.monit.memory)}`);
|
|
469
|
+
console.log(` CPU : ${p.monit.cpu.toFixed(1)}%`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function cmdLogs(args: string[]) {
|
|
474
|
+
const target = args[0] || "all";
|
|
475
|
+
let lines = 20;
|
|
476
|
+
const linesIdx = args.indexOf("--lines");
|
|
477
|
+
if (linesIdx !== -1 && args[linesIdx + 1]) lines = parseInt(args[linesIdx + 1]!);
|
|
478
|
+
|
|
479
|
+
const res = await sendToDaemon({ type: "logs", data: { target, lines } });
|
|
480
|
+
if (!res.success) {
|
|
481
|
+
console.error(colorize(`Error: ${res.error}`, "red"));
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
for (const log of res.data) {
|
|
485
|
+
console.log(colorize(`\n─── ${log.name} (id: ${log.id}) ───`, "bold"));
|
|
486
|
+
if (log.out) console.log(log.out);
|
|
487
|
+
if (log.err) console.log(colorize(log.err, "red"));
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function cmdFlush(args: string[]) {
|
|
492
|
+
const target = args[0];
|
|
493
|
+
const res = await sendToDaemon({ type: "flush", data: target ? { target } : undefined });
|
|
494
|
+
if (!res.success) {
|
|
495
|
+
console.error(colorize(`Error: ${res.error}`, "red"));
|
|
496
|
+
process.exit(1);
|
|
497
|
+
}
|
|
498
|
+
console.log(colorize("✓ Logs flushed", "green"));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function cmdScale(args: string[]) {
|
|
502
|
+
const target = args[0];
|
|
503
|
+
const count = parseInt(args[1]!);
|
|
504
|
+
if (!target || isNaN(count)) {
|
|
505
|
+
console.error(colorize("Usage: bm2 scale <name|id> <count>", "red"));
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
const res = await sendToDaemon({ type: "scale", data: { target, count } });
|
|
509
|
+
if (!res.success) {
|
|
510
|
+
console.error(colorize(`Error: ${res.error}`, "red"));
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
printProcessTable(res.data);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function cmdSave() {
|
|
517
|
+
const res = await sendToDaemon({ type: "save" });
|
|
518
|
+
if (!res.success) {
|
|
519
|
+
console.error(colorize(`Error: ${res.error}`, "red"));
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
console.log(colorize("✓ Process list saved", "green"));
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async function cmdResurrect() {
|
|
526
|
+
const res = await sendToDaemon({ type: "resurrect" });
|
|
527
|
+
if (!res.success) {
|
|
528
|
+
console.error(colorize(`Error: ${res.error}`, "red"));
|
|
529
|
+
process.exit(1);
|
|
530
|
+
}
|
|
531
|
+
printProcessTable(res.data);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function cmdKill() {
|
|
535
|
+
try {
|
|
536
|
+
await sendToDaemon({ type: "kill" });
|
|
537
|
+
} catch { /* ignore */ }
|
|
538
|
+
await Bun.sleep(500);
|
|
539
|
+
cleanupStaleFiles();
|
|
540
|
+
console.log(colorize("✓ Daemon killed", "green"));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async function cmdMonit() {
|
|
544
|
+
const res = await sendToDaemon({ type: "metrics" });
|
|
545
|
+
if (!res.success) {
|
|
546
|
+
console.error(colorize(`Error: ${res.error}`, "red"));
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
const snapshot = res.data;
|
|
550
|
+
console.log(colorize("\n⚡ BM2 Monitor\n", "bold"));
|
|
551
|
+
console.log(`System: CPU ${snapshot.system.cpuCount} cores | Load ${snapshot.system.loadAvg[0]?.toFixed(2)}`);
|
|
552
|
+
for (const p of snapshot.processes) {
|
|
553
|
+
console.log(`${padRight(p.name, 20)} ${colorize(p.status, statusColor(p.status))} CPU: ${p.cpu.toFixed(1)}% Mem: ${formatBytes(p.memory)}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// ---------------------------------------------------------------------------
|
|
558
|
+
// Main Dispatch
|
|
559
|
+
// ---------------------------------------------------------------------------
|
|
560
|
+
|
|
561
|
+
const args = process.argv.slice(2);
|
|
562
|
+
const command = args[0];
|
|
563
|
+
const commandArgs = args.slice(1);
|
|
564
|
+
|
|
565
|
+
switch (command) {
|
|
566
|
+
case "start": await cmdStart(commandArgs); break;
|
|
567
|
+
case "stop": await cmdStop(commandArgs); break;
|
|
568
|
+
case "restart": await cmdRestart(commandArgs); break;
|
|
569
|
+
case "reload": await cmdReload(commandArgs); break;
|
|
570
|
+
case "delete": case "rm": await cmdDelete(commandArgs); break;
|
|
571
|
+
case "scale": await cmdScale(commandArgs); break;
|
|
572
|
+
case "list": case "ls": await cmdList(); break;
|
|
573
|
+
case "describe": await cmdDescribe(commandArgs); break;
|
|
574
|
+
case "logs": await cmdLogs(commandArgs); break;
|
|
575
|
+
case "flush": await cmdFlush(commandArgs); break;
|
|
576
|
+
case "monit": await cmdMonit(); break;
|
|
577
|
+
case "save": await cmdSave(); break;
|
|
578
|
+
case "resurrect": await cmdResurrect(); break;
|
|
579
|
+
case "kill": await cmdKill(); break;
|
|
580
|
+
case "help":
|
|
581
|
+
case undefined:
|
|
582
|
+
console.log(`BM2 v${VERSION} - Usage: bm2 <command> [options]`);
|
|
583
|
+
break;
|
|
584
|
+
default:
|
|
585
|
+
console.error(colorize(`Unknown command: ${command}`, "red"));
|
|
586
|
+
process.exit(1);
|
|
587
|
+
}
|
|
File without changes
|