@irsyadulibad/servermon 1.2.0 ā 1.2.2
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/cli/index.ts +8 -10
- package/src/cli/service.ts +7 -20
- package/src/config/index.ts +57 -50
- package/src/daemon/index.ts +18 -18
- package/src/types/index.ts +8 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@irsyadulibad/servermon",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Lightweight server monitoring daemon ā collects system metrics and sends structured reports to Telegram. Built with Bun + TypeScript.",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"type": "module",
|
package/src/cli/index.ts
CHANGED
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
loadConfig,
|
|
5
5
|
saveConfig,
|
|
6
6
|
configPath,
|
|
7
|
-
configFile,
|
|
8
7
|
listServers,
|
|
9
8
|
deleteConfig,
|
|
10
9
|
} from "../config";
|
|
@@ -19,7 +18,7 @@ import pkg from "../../package.json";
|
|
|
19
18
|
async function interactiveSetup(name?: string): Promise<void> {
|
|
20
19
|
const tag = name ? ` [${name}]` : "";
|
|
21
20
|
console.log(`š„ Server Monitor ā First Time Setup${tag}`);
|
|
22
|
-
console.log(` Config will be saved to: ${
|
|
21
|
+
console.log(` Config will be saved to: ${configPath()}\n`);
|
|
23
22
|
|
|
24
23
|
const token = prompt("š Telegram Bot Token: ")?.trim();
|
|
25
24
|
if (!token) {
|
|
@@ -65,7 +64,7 @@ async function interactiveSetup(name?: string): Promise<void> {
|
|
|
65
64
|
|
|
66
65
|
await saveConfig({ token, interval, name });
|
|
67
66
|
console.log(`\nā
Config saved!`);
|
|
68
|
-
console.log(` š ${
|
|
67
|
+
console.log(` š ${configPath()}`);
|
|
69
68
|
console.log(` ā± Interval: ${interval}s (${label})`);
|
|
70
69
|
const serverTag = name ? ` --name ${name}` : "";
|
|
71
70
|
console.log(
|
|
@@ -120,12 +119,12 @@ export function createApp(): Crust {
|
|
|
120
119
|
process.env["TELEGRAM_BOT_TOKEN"] = config.token;
|
|
121
120
|
process.env["MONITOR_INTERVAL"] = String(config.interval);
|
|
122
121
|
if (config.chatId) process.env["TELEGRAM_CHAT_ID"] = config.chatId;
|
|
123
|
-
if (
|
|
122
|
+
if (name) process.env["SERVER_NAME"] = name;
|
|
124
123
|
|
|
125
|
-
console.log(`š Config: ${configPath(
|
|
124
|
+
console.log(`š Config: ${configPath()}`);
|
|
126
125
|
console.log(`š” Bot: ...${config.token.slice(-8)}`);
|
|
127
126
|
if (config.chatId) console.log(`š¬ Chat: ${config.chatId}`);
|
|
128
|
-
if (
|
|
127
|
+
if (name) console.log(`š· Name: ${name}`);
|
|
129
128
|
console.log();
|
|
130
129
|
|
|
131
130
|
const { start } = await import("../daemon");
|
|
@@ -162,7 +161,7 @@ export function createApp(): Crust {
|
|
|
162
161
|
console.log(`š” Bot: ...${config.token.slice(-8)} š¬ ${config.chatId}\n`);
|
|
163
162
|
|
|
164
163
|
const { sendReport } = await import("../reporter");
|
|
165
|
-
const ok = await sendReport(config.token, config.chatId,
|
|
164
|
+
const ok = await sendReport(config.token, config.chatId, name ?? undefined);
|
|
166
165
|
console.log(ok ? "ā
Report sent!" : "ā Failed to send report");
|
|
167
166
|
})
|
|
168
167
|
)
|
|
@@ -179,10 +178,9 @@ export function createApp(): Crust {
|
|
|
179
178
|
console.log("š Configured Servers:");
|
|
180
179
|
for (const s of servers) {
|
|
181
180
|
const cfg = await loadConfig(s === "default" ? undefined : s);
|
|
182
|
-
const nameTag = cfg?.name ? ` (--name ${cfg.name})` : "";
|
|
183
181
|
const interval = cfg?.interval ?? "?";
|
|
184
182
|
const chatLabel = cfg?.chatId ? ` š¬ ${cfg.chatId}` : " ā not yet paired";
|
|
185
|
-
console.log(` ⢠${s}
|
|
183
|
+
console.log(` ⢠${s}`);
|
|
186
184
|
console.log(` ā± ${interval}s ${chatLabel}`);
|
|
187
185
|
}
|
|
188
186
|
})
|
|
@@ -216,7 +214,7 @@ export function createApp(): Crust {
|
|
|
216
214
|
}
|
|
217
215
|
|
|
218
216
|
console.log(`ā ļø You are about to delete server config: "${rawName}"`);
|
|
219
|
-
console.log(` š ${
|
|
217
|
+
console.log(` š ${configPath()}`);
|
|
220
218
|
console.log(` š¤ Bot: ...${cfg.token.slice(-8)}`);
|
|
221
219
|
if (cfg.chatId) console.log(` š¬ Chat: ${cfg.chatId}`);
|
|
222
220
|
console.log("\n This action cannot be undone.\n");
|
package/src/cli/service.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Crust } from "@crustjs/core";
|
|
2
|
-
import {
|
|
2
|
+
import { listServers, configPath } from "../config";
|
|
3
3
|
|
|
4
4
|
const HOME = process.env.HOME ?? "~";
|
|
5
5
|
const SYSTEMD_DIR = `${HOME}/.config/systemd/user`;
|
|
@@ -54,9 +54,9 @@ function getServiceStatus(): string {
|
|
|
54
54
|
/* ------------------------------------------------------------------ */
|
|
55
55
|
|
|
56
56
|
async function cmdInstall(): Promise<void> {
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
59
|
-
console.error("ā No
|
|
57
|
+
const servers = await listServers();
|
|
58
|
+
if (servers.length === 0) {
|
|
59
|
+
console.error("ā No servers configured. Run `servermon setup` or `servermon setup --name <name>` first.");
|
|
60
60
|
process.exit(1);
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -74,25 +74,12 @@ async function cmdInstall(): Promise<void> {
|
|
|
74
74
|
|
|
75
75
|
console.log(`š bun: ${bunPath}`);
|
|
76
76
|
console.log(`š servermon: ${servermonPath}`);
|
|
77
|
-
console.log(`š Config: ${configPath()}
|
|
77
|
+
console.log(`š Config: ${configPath()}`);
|
|
78
|
+
console.log(`š Servers: ${servers.join(", ")}\n`);
|
|
78
79
|
|
|
79
80
|
await ensureSystemdDir();
|
|
80
81
|
|
|
81
|
-
const serviceContent = `[Unit]
|
|
82
|
-
Description=Server Monitor ā Telegram system health reports
|
|
83
|
-
After=network-online.target
|
|
84
|
-
Wants=network-online.target
|
|
85
|
-
|
|
86
|
-
[Service]
|
|
87
|
-
Type=simple
|
|
88
|
-
ExecStart=${servermonPath} start
|
|
89
|
-
Restart=always
|
|
90
|
-
RestartSec=30
|
|
91
|
-
Environment=NODE_ENV=production
|
|
92
|
-
|
|
93
|
-
[Install]
|
|
94
|
-
WantedBy=default.target
|
|
95
|
-
`;
|
|
82
|
+
const serviceContent = `[Unit]\nDescription=Server Monitor ā Telegram system health reports\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nExecStart=${servermonPath} start\nRestart=always\nRestartSec=30\nEnvironment=NODE_ENV=production\n\n[Install]\nWantedBy=default.target\n`;
|
|
96
83
|
|
|
97
84
|
await Bun.write(SERVICE_PATH, serviceContent);
|
|
98
85
|
console.log(`š Written: ${SERVICE_PATH}`);
|
package/src/config/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { homedir } from "os";
|
|
2
2
|
import { join } from "path";
|
|
3
|
-
import { mkdir,
|
|
3
|
+
import { mkdir, unlink } from "fs/promises";
|
|
4
4
|
import { existsSync } from "fs";
|
|
5
|
-
import type { ServerMonConfig } from "../types";
|
|
5
|
+
import type { ServerMonConfig, ServerEntry } from "../types";
|
|
6
6
|
|
|
7
7
|
const CONFIG_DIR = join(homedir(), ".irsyadulibad", "servermon");
|
|
8
8
|
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
@@ -11,12 +11,8 @@ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
|
11
11
|
/* Helpers */
|
|
12
12
|
/* ------------------------------------------------------------------ */
|
|
13
13
|
|
|
14
|
-
export function
|
|
15
|
-
return
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function configPath(name?: string): string {
|
|
19
|
-
return configFile(name);
|
|
14
|
+
export function configPath(): string {
|
|
15
|
+
return CONFIG_FILE;
|
|
20
16
|
}
|
|
21
17
|
|
|
22
18
|
export function configDir(): string {
|
|
@@ -24,61 +20,72 @@ export function configDir(): string {
|
|
|
24
20
|
}
|
|
25
21
|
|
|
26
22
|
/* ------------------------------------------------------------------ */
|
|
27
|
-
/*
|
|
23
|
+
/* Internal helpers */
|
|
28
24
|
/* ------------------------------------------------------------------ */
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
await mkdir(CONFIG_DIR, { recursive: true });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function loadConfig(name?: string): Promise<ServerMonConfig | null> {
|
|
26
|
+
async function readConfig(): Promise<ServerMonConfig | null> {
|
|
35
27
|
try {
|
|
36
|
-
const file = Bun.file(
|
|
28
|
+
const file = Bun.file(CONFIG_FILE);
|
|
37
29
|
if (!(await file.exists())) return null;
|
|
38
|
-
const data = await file.json();
|
|
39
|
-
if (!data?.
|
|
40
|
-
return
|
|
41
|
-
token: String(data.token),
|
|
42
|
-
interval: Math.max(30, parseInt(String(data.interval)) || 300),
|
|
43
|
-
chatId: data.chatId ? String(data.chatId) : undefined,
|
|
44
|
-
name: data.name ? String(data.name) : name,
|
|
45
|
-
};
|
|
30
|
+
const data = (await file.json()) as ServerMonConfig;
|
|
31
|
+
if (!data?.servers || typeof data.servers !== "object") return null;
|
|
32
|
+
return data;
|
|
46
33
|
} catch {
|
|
47
34
|
return null;
|
|
48
35
|
}
|
|
49
36
|
}
|
|
50
37
|
|
|
51
|
-
|
|
52
|
-
await
|
|
53
|
-
await Bun.write(
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export async function deleteConfig(name?: string): Promise<boolean> {
|
|
57
|
-
const file = configFile(name);
|
|
58
|
-
if (!existsSync(file)) return false;
|
|
59
|
-
try {
|
|
60
|
-
await unlink(file);
|
|
61
|
-
return true;
|
|
62
|
-
} catch {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
38
|
+
async function writeConfig(config: ServerMonConfig): Promise<void> {
|
|
39
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
40
|
+
await Bun.write(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
65
41
|
}
|
|
66
42
|
|
|
67
43
|
/* ------------------------------------------------------------------ */
|
|
68
|
-
/*
|
|
44
|
+
/* CRUD */
|
|
69
45
|
/* ------------------------------------------------------------------ */
|
|
70
46
|
|
|
47
|
+
export async function ensureConfigDir(): Promise<void> {
|
|
48
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function loadConfig(name?: string): Promise<ServerEntry | null> {
|
|
52
|
+
const cfg = await readConfig();
|
|
53
|
+
if (!cfg) return null;
|
|
54
|
+
const key = name ?? "default";
|
|
55
|
+
const entry = cfg.servers[key];
|
|
56
|
+
if (!entry?.token) return null;
|
|
57
|
+
return {
|
|
58
|
+
token: String(entry.token),
|
|
59
|
+
interval: Math.max(30, parseInt(String(entry.interval)) || 300),
|
|
60
|
+
chatId: entry.chatId ? String(entry.chatId) : undefined,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function saveConfig(
|
|
65
|
+
entry: ServerEntry & { name?: string },
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
const cfg = (await readConfig()) ?? { servers: {} };
|
|
68
|
+
const key = entry.name ?? "default";
|
|
69
|
+
cfg.servers[key] = {
|
|
70
|
+
token: entry.token,
|
|
71
|
+
interval: entry.interval,
|
|
72
|
+
chatId: entry.chatId,
|
|
73
|
+
};
|
|
74
|
+
await writeConfig(cfg);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function deleteConfig(name?: string): Promise<boolean> {
|
|
78
|
+
const cfg = await readConfig();
|
|
79
|
+
if (!cfg) return false;
|
|
80
|
+
const key = name ?? "default";
|
|
81
|
+
if (!cfg.servers[key]) return false;
|
|
82
|
+
delete cfg.servers[key];
|
|
83
|
+
await writeConfig(cfg);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
71
87
|
export async function listServers(): Promise<string[]> {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
for (const entry of entries) {
|
|
76
|
-
const match = entry.match(/^config-(.+)\.json$/);
|
|
77
|
-
if (match) names.push(match[1]!);
|
|
78
|
-
}
|
|
79
|
-
} catch {
|
|
80
|
-
// dir may not exist yet
|
|
81
|
-
}
|
|
82
|
-
if (existsSync(CONFIG_FILE)) names.unshift("default");
|
|
83
|
-
return names;
|
|
88
|
+
const cfg = await readConfig();
|
|
89
|
+
if (!cfg) return [];
|
|
90
|
+
return Object.keys(cfg.servers).sort();
|
|
84
91
|
}
|
package/src/daemon/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { sendReport } from "../reporter";
|
|
2
2
|
import { saveConfig, loadConfig, listServers } from "../config";
|
|
3
|
-
import type { NamedConfig } from "../types";
|
|
3
|
+
import type { ServerEntry, NamedConfig } from "../types";
|
|
4
4
|
|
|
5
5
|
/* ------------------------------------------------------------------ */
|
|
6
6
|
/* Environment variables (set by cli/) */
|
|
@@ -57,10 +57,10 @@ async function autoDetectChatId(token: string): Promise<string | null> {
|
|
|
57
57
|
|
|
58
58
|
async function persistChatId(id: string, name?: string): Promise<void> {
|
|
59
59
|
try {
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
await saveConfig(
|
|
60
|
+
const entry = await loadConfig(name);
|
|
61
|
+
if (entry) {
|
|
62
|
+
entry.chatId = id;
|
|
63
|
+
await saveConfig({ ...entry, name });
|
|
64
64
|
console.log("š¾ Chat ID saved to config");
|
|
65
65
|
}
|
|
66
66
|
} catch {
|
|
@@ -138,30 +138,30 @@ export async function startAll(): Promise<void> {
|
|
|
138
138
|
console.log(`š Found ${servers.length} server(s) to monitor:\n`);
|
|
139
139
|
|
|
140
140
|
const configs: NamedConfig[] = [];
|
|
141
|
-
for (const
|
|
142
|
-
const
|
|
143
|
-
if (!
|
|
144
|
-
console.log(` ā ļø ${
|
|
141
|
+
for (const name of servers) {
|
|
142
|
+
const entry = await loadConfig(name);
|
|
143
|
+
if (!entry) {
|
|
144
|
+
console.log(` ā ļø ${name}: config unreadable, skipping`);
|
|
145
145
|
continue;
|
|
146
146
|
}
|
|
147
|
-
if (!
|
|
147
|
+
if (!entry.chatId) {
|
|
148
148
|
if (configs.length === 0) {
|
|
149
|
-
console.log(`š ${
|
|
150
|
-
const detected = await autoDetectChatId(
|
|
149
|
+
console.log(`š ${name}: Chat ID not set ā auto-detecting...`);
|
|
150
|
+
const detected = await autoDetectChatId(entry.token);
|
|
151
151
|
if (detected) {
|
|
152
|
-
|
|
153
|
-
await saveConfig({ ...
|
|
154
|
-
console.log(`š¾ ${
|
|
152
|
+
entry.chatId = detected;
|
|
153
|
+
await saveConfig({ ...entry, name });
|
|
154
|
+
console.log(`š¾ ${name}: Chat ID ${detected} saved`);
|
|
155
155
|
} else {
|
|
156
|
-
console.log(` ā ${
|
|
156
|
+
console.log(` ā ${name}: no chat ID yet. DM your bot first then restart.`);
|
|
157
157
|
continue;
|
|
158
158
|
}
|
|
159
159
|
} else {
|
|
160
|
-
console.log(` ā ${
|
|
160
|
+
console.log(` ā ${name}: no chat ID. Run 'servermon start --name ${name}' first.`);
|
|
161
161
|
continue;
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
|
-
configs.push({ name
|
|
164
|
+
configs.push({ name, cfg: entry });
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
if (configs.length === 0) {
|
package/src/types/index.ts
CHANGED
|
@@ -56,16 +56,20 @@ export interface SystemMetrics {
|
|
|
56
56
|
temperature: number | null; // celsius
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
/**
|
|
60
|
-
export interface
|
|
59
|
+
/** A single server entry in the config */
|
|
60
|
+
export interface ServerEntry {
|
|
61
61
|
token: string;
|
|
62
62
|
interval: number;
|
|
63
63
|
chatId?: string;
|
|
64
|
-
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Top-level config persisted to disk ā single file, multiple servers */
|
|
67
|
+
export interface ServerMonConfig {
|
|
68
|
+
servers: Record<string, ServerEntry>;
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
/** A named config loaded at runtime ā internal for multi-server loop */
|
|
68
72
|
export interface NamedConfig {
|
|
69
73
|
name: string;
|
|
70
|
-
cfg:
|
|
74
|
+
cfg: ServerEntry;
|
|
71
75
|
}
|