@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@irsyadulibad/servermon",
3
- "version": "1.2.0",
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: ${configFile(name)}\n`);
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(` šŸ“ ${configFile(name)}`);
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 (config.name) process.env["SERVER_NAME"] = config.name;
122
+ if (name) process.env["SERVER_NAME"] = name;
124
123
 
125
- console.log(`šŸ“ Config: ${configPath(config.name)}`);
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 (config.name) console.log(`šŸ· Name: ${config.name}`);
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, config.name);
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}${nameTag}`);
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(` šŸ“ ${configFile(name)}`);
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");
@@ -1,5 +1,5 @@
1
1
  import type { Crust } from "@crustjs/core";
2
- import { loadConfig, configPath } from "../config";
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 config = await loadConfig();
58
- if (!config) {
59
- console.error("āŒ No config found. Run `servermon setup` first.");
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()}\n`);
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}`);
@@ -1,8 +1,8 @@
1
1
  import { homedir } from "os";
2
2
  import { join } from "path";
3
- import { mkdir, readdir, unlink } from "fs/promises";
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 configFile(name?: string): string {
15
- return name ? join(CONFIG_DIR, `config-${name}.json`) : CONFIG_FILE;
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
- /* CRUD */
23
+ /* Internal helpers */
28
24
  /* ------------------------------------------------------------------ */
29
25
 
30
- export async function ensureConfigDir(): Promise<void> {
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(configFile(name));
28
+ const file = Bun.file(CONFIG_FILE);
37
29
  if (!(await file.exists())) return null;
38
- const data = await file.json();
39
- if (!data?.token) return null;
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
- export async function saveConfig(config: ServerMonConfig): Promise<void> {
52
- await ensureConfigDir();
53
- await Bun.write(configFile(config.name), JSON.stringify(config, null, 2));
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
- /* Inventory */
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 names: string[] = [];
73
- try {
74
- const entries = await readdir(CONFIG_DIR);
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
  }
@@ -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 cfg = await loadConfig(name);
61
- if (cfg) {
62
- cfg.chatId = id;
63
- await saveConfig(cfg);
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 s of servers) {
142
- const cfg = await loadConfig(s === "default" ? undefined : s);
143
- if (!cfg) {
144
- console.log(` āš ļø ${s}: config unreadable, skipping`);
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 (!cfg.chatId) {
147
+ if (!entry.chatId) {
148
148
  if (configs.length === 0) {
149
- console.log(`šŸ” ${s}: Chat ID not set — auto-detecting...`);
150
- const detected = await autoDetectChatId(cfg.token);
149
+ console.log(`šŸ” ${name}: Chat ID not set — auto-detecting...`);
150
+ const detected = await autoDetectChatId(entry.token);
151
151
  if (detected) {
152
- cfg.chatId = detected;
153
- await saveConfig({ ...cfg, chatId: detected });
154
- console.log(`šŸ’¾ ${s}: Chat ID ${detected} saved`);
152
+ entry.chatId = detected;
153
+ await saveConfig({ ...entry, name });
154
+ console.log(`šŸ’¾ ${name}: Chat ID ${detected} saved`);
155
155
  } else {
156
- console.log(` āŒ ${s}: no chat ID yet. DM your bot first then restart.`);
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(` āŒ ${s}: no chat ID. Run 'servermon start --name ${s}' first.`);
160
+ console.log(` āŒ ${name}: no chat ID. Run 'servermon start --name ${name}' first.`);
161
161
  continue;
162
162
  }
163
163
  }
164
- configs.push({ name: s, cfg });
164
+ configs.push({ name, cfg: entry });
165
165
  }
166
166
 
167
167
  if (configs.length === 0) {
@@ -56,16 +56,20 @@ export interface SystemMetrics {
56
56
  temperature: number | null; // celsius
57
57
  }
58
58
 
59
- /** ServerMon config persisted to disk */
60
- export interface ServerMonConfig {
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
- name?: string;
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: ServerMonConfig;
74
+ cfg: ServerEntry;
71
75
  }