@irsyadulibad/servermon 1.1.1 → 1.2.1

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.
@@ -0,0 +1,198 @@
1
+ import { sendReport } from "../reporter";
2
+ import { saveConfig, loadConfig, listServers } from "../config";
3
+ import type { ServerEntry, NamedConfig } from "../types";
4
+
5
+ /* ------------------------------------------------------------------ */
6
+ /* Environment variables (set by cli/) */
7
+ /* ------------------------------------------------------------------ */
8
+
9
+ const botToken = process.env["TELEGRAM_BOT_TOKEN"] ?? "";
10
+ let chatId = process.env["TELEGRAM_CHAT_ID"] ?? "";
11
+ const rawInterval = process.env["MONITOR_INTERVAL"] ?? "300";
12
+ const intervalSec = Math.max(30, parseInt(rawInterval) || 300);
13
+ const serverName = process.env["SERVER_NAME"] ?? "";
14
+
15
+ /* ------------------------------------------------------------------ */
16
+ /* Auto-detect Telegram chat ID */
17
+ /* ------------------------------------------------------------------ */
18
+
19
+ async function autoDetectChatId(token: string): Promise<string | null> {
20
+ try {
21
+ const resp = await fetch(`https://api.telegram.org/bot${token}/getUpdates?limit=5`);
22
+ const data = (await resp.json()) as {
23
+ ok: boolean;
24
+ result?: Array<{
25
+ message?: { chat?: { id: number; title?: string; first_name?: string } };
26
+ channel_post?: { chat?: { id: number; title?: string } };
27
+ }>;
28
+ };
29
+ if (!data.ok || !data.result?.length) return null;
30
+
31
+ const chatIds = new Set<string>();
32
+ for (const update of data.result.reverse()) {
33
+ const chat = update.message?.chat || update.channel_post?.chat;
34
+ if (chat?.id) chatIds.add(String(chat.id));
35
+ }
36
+ if (chatIds.size === 0) return null;
37
+
38
+ const id = [...chatIds][0]!;
39
+ const chatInfo = data.result.find(
40
+ (u) => String(u.message?.chat?.id || u.channel_post?.chat?.id) === id
41
+ );
42
+ const chatName =
43
+ chatInfo?.message?.chat?.title ||
44
+ chatInfo?.message?.chat?.first_name ||
45
+ chatInfo?.channel_post?.chat?.title ||
46
+ "Unknown";
47
+ console.log(`🔍 Auto-detected chat: ${chatName} (ID: ${id})`);
48
+ return id;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ /* ------------------------------------------------------------------ */
55
+ /* Persist chat ID to config file */
56
+ /* ------------------------------------------------------------------ */
57
+
58
+ async function persistChatId(id: string, name?: string): Promise<void> {
59
+ try {
60
+ const entry = await loadConfig(name);
61
+ if (entry) {
62
+ entry.chatId = id;
63
+ await saveConfig({ ...entry, name });
64
+ console.log("💾 Chat ID saved to config");
65
+ }
66
+ } catch {
67
+ // ok
68
+ }
69
+ }
70
+
71
+ /* ------------------------------------------------------------------ */
72
+ /* Single-server daemon */
73
+ /* ------------------------------------------------------------------ */
74
+
75
+ export async function start(): Promise<void> {
76
+ if (!botToken) {
77
+ console.error("❌ TELEGRAM_BOT_TOKEN not set. Run `servermon` first to configure.");
78
+ process.exit(1);
79
+ }
80
+
81
+ if (!chatId) {
82
+ console.log("🔍 TELEGRAM_CHAT_ID not set — auto-detecting...");
83
+ let detected = await autoDetectChatId(botToken);
84
+
85
+ if (!detected) {
86
+ console.log("⏳ Waiting for you to DM the bot on Telegram...");
87
+ console.log(" (polling every 10s — no restart needed)\n");
88
+
89
+ const POLL_INTERVAL_MS = 10_000;
90
+ while (!detected) {
91
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
92
+ detected = await autoDetectChatId(botToken);
93
+ }
94
+ }
95
+
96
+ chatId = detected;
97
+ await persistChatId(chatId, serverName || undefined);
98
+ }
99
+
100
+ console.log(`⏱ Interval: ${intervalSec}s (${(intervalSec / 60).toFixed(0)} menit)`);
101
+ console.log(`📡 Bot: ...${botToken.slice(-8)}`);
102
+ console.log(`💬 Chat: ${chatId}`);
103
+ if (serverName) console.log(`🏷 Server: ${serverName}`);
104
+ console.log();
105
+
106
+ async function tick() {
107
+ const start2 = Date.now();
108
+ const ok = await sendReport(botToken, chatId, serverName || undefined);
109
+ const elapsed = Date.now() - start2;
110
+ const ts = new Date().toLocaleString("id-ID", { timeZone: "Asia/Jakarta" });
111
+ console.log(`[${ts}] ${ok ? "✅" : "❌"} ${elapsed}ms`);
112
+ }
113
+
114
+ await tick();
115
+ setInterval(tick, intervalSec * 1000);
116
+
117
+ process.on("SIGINT", () => {
118
+ console.log("\n👋 Shutting down...");
119
+ process.exit(0);
120
+ });
121
+ process.on("SIGTERM", () => {
122
+ console.log("\n👋 Shutting down...");
123
+ process.exit(0);
124
+ });
125
+ }
126
+
127
+ /* ------------------------------------------------------------------ */
128
+ /* Multi-server daemon (runs all configured servers) */
129
+ /* ------------------------------------------------------------------ */
130
+
131
+ export async function startAll(): Promise<void> {
132
+ const servers = await listServers();
133
+ if (servers.length === 0) {
134
+ console.error("❌ No configured servers found. Run `servermon setup` first.");
135
+ process.exit(1);
136
+ }
137
+
138
+ console.log(`📋 Found ${servers.length} server(s) to monitor:\n`);
139
+
140
+ const configs: NamedConfig[] = [];
141
+ for (const name of servers) {
142
+ const entry = await loadConfig(name);
143
+ if (!entry) {
144
+ console.log(` ⚠️ ${name}: config unreadable, skipping`);
145
+ continue;
146
+ }
147
+ if (!entry.chatId) {
148
+ if (configs.length === 0) {
149
+ console.log(`🔍 ${name}: Chat ID not set — auto-detecting...`);
150
+ const detected = await autoDetectChatId(entry.token);
151
+ if (detected) {
152
+ entry.chatId = detected;
153
+ await saveConfig({ ...entry, name });
154
+ console.log(`💾 ${name}: Chat ID ${detected} saved`);
155
+ } else {
156
+ console.log(` ❌ ${name}: no chat ID yet. DM your bot first then restart.`);
157
+ continue;
158
+ }
159
+ } else {
160
+ console.log(` ❌ ${name}: no chat ID. Run 'servermon start --name ${name}' first.`);
161
+ continue;
162
+ }
163
+ }
164
+ configs.push({ name, cfg: entry });
165
+ }
166
+
167
+ if (configs.length === 0) {
168
+ console.error("❌ No servers ready to monitor.");
169
+ process.exit(1);
170
+ }
171
+
172
+ console.log();
173
+ console.log(`🔄 Monitoring ${configs.length} server(s) — reports every ${intervalSec}s`);
174
+ console.log();
175
+
176
+ async function tickAll() {
177
+ const ts = new Date().toLocaleString("id-ID", { timeZone: "Asia/Jakarta" });
178
+ for (const { name, cfg } of configs) {
179
+ if (!cfg.chatId) continue;
180
+ const start2 = Date.now();
181
+ const ok = await sendReport(cfg.token, cfg.chatId, name === "default" ? undefined : name);
182
+ const elapsed = Date.now() - start2;
183
+ console.log(`[${ts}] ${ok ? "✅" : "❌"} ${name} — ${elapsed}ms`);
184
+ }
185
+ }
186
+
187
+ await tickAll();
188
+ setInterval(tickAll, intervalSec * 1000);
189
+
190
+ process.on("SIGINT", () => {
191
+ console.log("\n👋 Shutting down...");
192
+ process.exit(0);
193
+ });
194
+ process.on("SIGTERM", () => {
195
+ console.log("\n👋 Shutting down...");
196
+ process.exit(0);
197
+ });
198
+ }
@@ -1,56 +1,41 @@
1
1
  import * as os from "os";
2
+ import type { DiskInfo, ProcInfo, SystemMetrics } from "../types";
2
3
 
3
- export interface CPUMetrics {
4
- model: string;
5
- cores: number;
6
- loadAvg: { "1min": number; "5min": number; "15min": number };
7
- usagePercent: number;
8
- }
4
+ /* ------------------------------------------------------------------ */
5
+ /* Helpers */
6
+ /* ------------------------------------------------------------------ */
9
7
 
10
- export interface MemoryMetrics {
11
- total: number;
12
- used: number;
13
- free: number;
14
- usagePercent: number;
15
- swapTotal: number;
16
- swapUsed: number;
17
- swapUsagePercent: number;
8
+ export function formatBytes(bytes: number): string {
9
+ if (bytes >= 1_073_741_824) return `${(bytes / 1_073_741_824).toFixed(1)} GiB`;
10
+ if (bytes >= 1_048_576) return `${(bytes / 1_048_576).toFixed(1)} MiB`;
11
+ if (bytes >= 1_024) return `${(bytes / 1_024).toFixed(1)} KiB`;
12
+ return `${bytes} B`;
18
13
  }
19
14
 
20
- export interface DiskInfo {
21
- mount: string;
22
- total: number;
23
- used: number;
24
- available: number;
25
- usagePercent: number;
15
+ export function formatRate(bytesPerSec: number): string {
16
+ if (bytesPerSec >= 1_048_576) return `${(bytesPerSec / 1_048_576).toFixed(2)} MB/s`;
17
+ if (bytesPerSec >= 1_024) return `${(bytesPerSec / 1_024).toFixed(1)} KB/s`;
18
+ return `${bytesPerSec} B/s`;
26
19
  }
27
20
 
28
- export interface NetworkMetrics {
29
- rxRate: number; // bytes/sec
30
- txRate: number;
31
- rxTotal: number;
32
- txTotal: number;
21
+ export function formatUptime(seconds: number): string {
22
+ const d = Math.floor(seconds / 86400);
23
+ const h = Math.floor((seconds % 86400) / 3600);
24
+ const m = Math.floor((seconds % 3600) / 60);
25
+ const parts: string[] = [];
26
+ if (d > 0) parts.push(`${d}d`);
27
+ if (h > 0) parts.push(`${h}h`);
28
+ parts.push(`${m}m`);
29
+ return parts.join(" ");
33
30
  }
34
31
 
35
- export interface ProcInfo {
36
- pid: number;
37
- name: string;
38
- cpuPercent: number;
39
- memPercent: number;
32
+ function cpuTicks(stat: string): number[] {
33
+ return stat.split(/\s+/).slice(1).map(Number);
40
34
  }
41
35
 
42
- export interface SystemMetrics {
43
- hostname: string;
44
- platform: string;
45
- arch: string;
46
- uptime: number;
47
- cpu: CPUMetrics;
48
- memory: MemoryMetrics;
49
- disks: DiskInfo[];
50
- network: NetworkMetrics;
51
- topProcs: ProcInfo[];
52
- temperature: number | null; // celsius
53
- }
36
+ /* ------------------------------------------------------------------ */
37
+ /* Low-level collectors */
38
+ /* ------------------------------------------------------------------ */
54
39
 
55
40
  async function exec(cmd: string): Promise<string> {
56
41
  const proc = Bun.spawn(["bash", "-c", cmd], { stdout: "pipe" });
@@ -77,9 +62,7 @@ async function readNetDev(): Promise<{ rx: number; tx: number }> {
77
62
  for (const line of data.split("\n")) {
78
63
  if (!line.includes(":")) continue;
79
64
  const ifname = line.split(":")[0]!.trim();
80
- // Skip loopback
81
65
  if (ifname === "lo") continue;
82
- // Skip veth, docker
83
66
  if (ifname.startsWith("veth") || ifname.startsWith("docker") || ifname.startsWith("br-"))
84
67
  continue;
85
68
  const parts = line.split(":")[1]!.trim().split(/\s+/);
@@ -95,14 +78,6 @@ async function readTemperature(): Promise<number | null> {
95
78
  `for z in /sys/class/thermal/thermal_zone*/temp; do [ -r "$z" ] && echo "$z=$(cat "$z")"; done 2>/dev/null`
96
79
  );
97
80
  if (!zones) return null;
98
- let best = Number.MAX_VALUE;
99
- for (const line of zones.split("\n")) {
100
- const val = parseInt(line.split("=")[1]!);
101
- // thermal_zone0 often the CPU package; pick the highest non-zero temp
102
- if (val > 0 && val < best) best = val;
103
- // Actually we want the HIGHEST, not lowest
104
- }
105
- // Reread — pick highest
106
81
  let highest = -Infinity;
107
82
  for (const line of zones.split("\n")) {
108
83
  const val = parseInt(line.split("=")[1]!);
@@ -114,6 +89,10 @@ async function readTemperature(): Promise<number | null> {
114
89
  }
115
90
  }
116
91
 
92
+ /* ------------------------------------------------------------------ */
93
+ /* Public API */
94
+ /* ------------------------------------------------------------------ */
95
+
117
96
  export async function collectMetrics(): Promise<SystemMetrics> {
118
97
  // --- disk ---
119
98
  const dfOutput = await exec("df -P -B1 / /home 2>/dev/null");
@@ -141,9 +120,6 @@ export async function collectMetrics(): Promise<SystemMetrics> {
141
120
  await new Promise((r) => setTimeout(r, 500));
142
121
  const stat2 = await exec("cat /proc/stat | grep '^cpu '");
143
122
 
144
- function cpuTicks(stat: string): number[] {
145
- return stat.split(/\s+/).slice(1).map(Number);
146
- }
147
123
  const t1 = cpuTicks(stat1);
148
124
  const t2 = cpuTicks(stat2);
149
125
  let usagePercent = 0;
@@ -161,7 +137,7 @@ export async function collectMetrics(): Promise<SystemMetrics> {
161
137
  const net1 = await readNetDev();
162
138
  await new Promise((r) => setTimeout(r, 1000));
163
139
  const net2 = await readNetDev();
164
- const rxRate = Math.max(0, net2.rx - net1.rx); // bytes/sec
140
+ const rxRate = Math.max(0, net2.rx - net1.rx);
165
141
  const txRate = Math.max(0, net2.tx - net1.tx);
166
142
 
167
143
  // --- top processes ---
@@ -210,27 +186,3 @@ export async function collectMetrics(): Promise<SystemMetrics> {
210
186
  temperature,
211
187
  };
212
188
  }
213
-
214
- export function formatBytes(bytes: number): string {
215
- if (bytes >= 1_073_741_824) return `${(bytes / 1_073_741_824).toFixed(1)} GiB`;
216
- if (bytes >= 1_048_576) return `${(bytes / 1_048_576).toFixed(1)} MiB`;
217
- if (bytes >= 1_024) return `${(bytes / 1_024).toFixed(1)} KiB`;
218
- return `${bytes} B`;
219
- }
220
-
221
- export function formatRate(bytesPerSec: number): string {
222
- if (bytesPerSec >= 1_048_576) return `${(bytesPerSec / 1_048_576).toFixed(2)} MB/s`;
223
- if (bytesPerSec >= 1_024) return `${(bytesPerSec / 1_024).toFixed(1)} KB/s`;
224
- return `${bytesPerSec} B/s`;
225
- }
226
-
227
- export function formatUptime(seconds: number): string {
228
- const d = Math.floor(seconds / 86400);
229
- const h = Math.floor((seconds % 86400) / 3600);
230
- const m = Math.floor((seconds % 3600) / 60);
231
- const parts: string[] = [];
232
- if (d > 0) parts.push(`${d}d`);
233
- if (h > 0) parts.push(`${h}h`);
234
- parts.push(`${m}m`);
235
- return parts.join(" ");
236
- }
@@ -1,11 +1,14 @@
1
- import { formatBytes, formatRate, formatUptime, type SystemMetrics } from "./monitor";
1
+ import type { SystemMetrics } from "../types";
2
+ import { collectMetrics, formatBytes, formatRate, formatUptime } from "../monitor";
3
+
4
+ /* ------------------------------------------------------------------ */
5
+ /* Helpers */
6
+ /* ------------------------------------------------------------------ */
2
7
 
3
- // HTML escape
4
8
  function esc(s: string): string {
5
9
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
6
10
  }
7
11
 
8
- // Bar chart with inline colored blocks
9
12
  function bar(percent: number, w = 10): string {
10
13
  const filled = Math.min(w, Math.max(0, Math.round((percent / 100) * w)));
11
14
  const empty = w - filled;
@@ -17,7 +20,6 @@ function pad(n: number, dp = 1): string {
17
20
  return n.toFixed(dp).padStart(5);
18
21
  }
19
22
 
20
- // Overall health
21
23
  function healthTag(m: SystemMetrics): string {
22
24
  const d = m.disks.length ? Math.max(...m.disks.map((x) => x.usagePercent)) : 0;
23
25
  if (m.cpu.usagePercent > 85 || m.memory.usagePercent > 95 || d > 95) return "🚨 CRITICAL";
@@ -25,7 +27,11 @@ function healthTag(m: SystemMetrics): string {
25
27
  return "✅ HEALTHY";
26
28
  }
27
29
 
28
- export function formatReportHTML(m: SystemMetrics): string {
30
+ /* ------------------------------------------------------------------ */
31
+ /* Formatter */
32
+ /* ------------------------------------------------------------------ */
33
+
34
+ export function formatReportHTML(m: SystemMetrics, serverName?: string): string {
29
35
  const now = new Date().toLocaleString("id-ID", {
30
36
  timeZone: "Asia/Jakarta",
31
37
  day: "numeric",
@@ -36,21 +42,19 @@ export function formatReportHTML(m: SystemMetrics): string {
36
42
  });
37
43
 
38
44
  const tempStr = m.temperature !== null ? ` 🌡 ${m.temperature.toFixed(0)}°C` : "";
45
+ const nameTag = serverName ? ` [${esc(serverName)}]` : "";
39
46
 
40
- // ── Header ──
41
47
  const header = [
42
- `<b>🖥 ${esc(m.hostname)}</b> — ${healthTag(m)}`,
48
+ `<b>🖥 ${esc(m.hostname)}</b>${nameTag} — ${healthTag(m)}`,
43
49
  `📅 ${esc(now)} │ ⏱ ${esc(formatUptime(m.uptime))}${tempStr}`,
44
50
  `🐧 ${esc(m.platform)} ${esc(m.arch)} │ ${esc(m.cpu.model)} (${m.cpu.cores}c)`,
45
51
  ];
46
52
 
47
- // ── CPU card ──
48
53
  const cpu = [
49
54
  `<b>💻 CPU</b> <code>${pad(m.cpu.usagePercent)}%</code> ${bar(m.cpu.usagePercent)}`,
50
55
  ` Load: <code>${m.cpu.loadAvg["1min"].toFixed(2)}</code> / <code>${m.cpu.loadAvg["5min"].toFixed(2)}</code> / <code>${m.cpu.loadAvg["15min"].toFixed(2)}</code>`,
51
56
  ];
52
57
 
53
- // ── Memory card ──
54
58
  const mem = [
55
59
  `<b>🧠 RAM</b> <code>${pad(m.memory.usagePercent)}%</code> ${bar(m.memory.usagePercent)}`,
56
60
  ` <code>${esc(formatBytes(m.memory.used))}</code> / <code>${esc(formatBytes(m.memory.total))}</code>`,
@@ -59,7 +63,6 @@ export function formatReportHTML(m: SystemMetrics): string {
59
63
  : "",
60
64
  ].filter(Boolean);
61
65
 
62
- // ── Disk card ──
63
66
  const disks = [`<b>💾 DISK</b>`];
64
67
  for (const d of m.disks) {
65
68
  disks.push(
@@ -70,13 +73,11 @@ export function formatReportHTML(m: SystemMetrics): string {
70
73
  );
71
74
  }
72
75
 
73
- // ── Network card ──
74
76
  const net = [
75
77
  `<b>🌐 NET</b>`,
76
78
  ` ↓ <code>${esc(formatRate(m.network.rxRate))}</code> ↑ <code>${esc(formatRate(m.network.txRate))}</code>`,
77
79
  ];
78
80
 
79
- // ── Top processes ──
80
81
  const procLines: string[] = [];
81
82
  if (m.topProcs.length > 0) {
82
83
  procLines.push(`<b>📊 TOP PROCESSES</b>`);
@@ -87,7 +88,6 @@ export function formatReportHTML(m: SystemMetrics): string {
87
88
  }
88
89
  }
89
90
 
90
- // ── Alerts ──
91
91
  const alerts: string[] = [];
92
92
  if (m.cpu.usagePercent > 85) alerts.push(`🔴 CPU tinggi: ${m.cpu.usagePercent.toFixed(1)}%`);
93
93
  if (m.memory.usagePercent > 90)
@@ -98,10 +98,7 @@ export function formatReportHTML(m: SystemMetrics): string {
98
98
  if (d.usagePercent > 90)
99
99
  alerts.push(`🔴 Disk <code>${esc(d.mount)}</code>: ${d.usagePercent}%`);
100
100
  }
101
-
102
- if (alerts.length > 0) {
103
- alerts.unshift(`<b>⚠️ ALERTS</b>`);
104
- }
101
+ if (alerts.length > 0) alerts.unshift(`<b>⚠️ ALERTS</b>`);
105
102
 
106
103
  return [
107
104
  ...header,
@@ -122,6 +119,10 @@ export function formatReportHTML(m: SystemMetrics): string {
122
119
  .join("\n");
123
120
  }
124
121
 
122
+ /* ------------------------------------------------------------------ */
123
+ /* Telegram sender */
124
+ /* ------------------------------------------------------------------ */
125
+
125
126
  async function sendMessage(botToken: string, chatId: string, text: string): Promise<Response> {
126
127
  return fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
127
128
  method: "POST",
@@ -135,14 +136,18 @@ async function sendMessage(botToken: string, chatId: string, text: string): Prom
135
136
  });
136
137
  }
137
138
 
138
- export async function sendReport(botToken: string, chatId: string): Promise<boolean> {
139
+ export async function sendReport(
140
+ botToken: string,
141
+ chatId: string,
142
+ serverName?: string
143
+ ): Promise<boolean> {
139
144
  if (!botToken || !chatId) {
140
145
  console.error("❌ TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID are required.");
141
146
  return false;
142
147
  }
143
148
 
144
- const m = await import("./monitor").then((x) => x.collectMetrics());
145
- const report = formatReportHTML(m);
149
+ const m = await collectMetrics();
150
+ const report = formatReportHTML(m, serverName);
146
151
 
147
152
  // Telegram 4096 char limit — split on double newlines
148
153
  if (report.length > 4000) {
@@ -0,0 +1,75 @@
1
+ /** CPU metrics */
2
+ export interface CPUMetrics {
3
+ model: string;
4
+ cores: number;
5
+ loadAvg: { "1min": number; "5min": number; "15min": number };
6
+ usagePercent: number;
7
+ }
8
+
9
+ /** Memory & swap metrics */
10
+ export interface MemoryMetrics {
11
+ total: number;
12
+ used: number;
13
+ free: number;
14
+ usagePercent: number;
15
+ swapTotal: number;
16
+ swapUsed: number;
17
+ swapUsagePercent: number;
18
+ }
19
+
20
+ /** Per-mount disk info */
21
+ export interface DiskInfo {
22
+ mount: string;
23
+ total: number;
24
+ used: number;
25
+ available: number;
26
+ usagePercent: number;
27
+ }
28
+
29
+ /** Network throughput */
30
+ export interface NetworkMetrics {
31
+ rxRate: number; // bytes/sec
32
+ txRate: number;
33
+ rxTotal: number;
34
+ txTotal: number;
35
+ }
36
+
37
+ /** Single process info */
38
+ export interface ProcInfo {
39
+ pid: number;
40
+ name: string;
41
+ cpuPercent: number;
42
+ memPercent: number;
43
+ }
44
+
45
+ /** Full system snapshot returned by collectMetrics() */
46
+ export interface SystemMetrics {
47
+ hostname: string;
48
+ platform: string;
49
+ arch: string;
50
+ uptime: number;
51
+ cpu: CPUMetrics;
52
+ memory: MemoryMetrics;
53
+ disks: DiskInfo[];
54
+ network: NetworkMetrics;
55
+ topProcs: ProcInfo[];
56
+ temperature: number | null; // celsius
57
+ }
58
+
59
+ /** A single server entry in the config */
60
+ export interface ServerEntry {
61
+ token: string;
62
+ interval: number;
63
+ chatId?: string;
64
+ }
65
+
66
+ /** Top-level config persisted to disk — single file, multiple servers */
67
+ export interface ServerMonConfig {
68
+ servers: Record<string, ServerEntry>;
69
+ }
70
+
71
+ /** A named config loaded at runtime — internal for multi-server loop */
72
+ export interface NamedConfig {
73
+ name: string;
74
+ cfg: ServerEntry;
75
+ }
package/src/config.ts DELETED
@@ -1,46 +0,0 @@
1
- import { homedir } from "os";
2
- import { join } from "path";
3
- import { mkdir } from "fs/promises";
4
-
5
- const CONFIG_DIR = join(homedir(), ".irsyadulibad", "servermon");
6
- const CONFIG_FILE = join(CONFIG_DIR, "config.json");
7
-
8
- export interface ServerMonConfig {
9
- token: string;
10
- interval: number;
11
- chatId?: string;
12
- }
13
-
14
- export async function ensureConfigDir(): Promise<void> {
15
- await mkdir(CONFIG_DIR, { recursive: true });
16
- }
17
-
18
- export async function loadConfig(): Promise<ServerMonConfig | null> {
19
- try {
20
- const file = Bun.file(CONFIG_FILE);
21
- if (!(await file.exists())) return null;
22
- const data = await file.json();
23
- // Minimal validation
24
- if (!data?.token) return null;
25
- return {
26
- token: String(data.token),
27
- interval: Math.max(30, parseInt(String(data.interval)) || 300),
28
- chatId: data.chatId ? String(data.chatId) : undefined,
29
- };
30
- } catch {
31
- return null;
32
- }
33
- }
34
-
35
- export async function saveConfig(config: ServerMonConfig): Promise<void> {
36
- await ensureConfigDir();
37
- await Bun.write(CONFIG_FILE, JSON.stringify(config, null, 2));
38
- }
39
-
40
- export function configPath(): string {
41
- return CONFIG_FILE;
42
- }
43
-
44
- export function configDir(): string {
45
- return CONFIG_DIR;
46
- }