@irsyadulibad/servermon 1.0.0
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/.env.example +14 -0
- package/.prettierignore +5 -0
- package/.prettierrc +10 -0
- package/README.md +202 -0
- package/bun.lock +209 -0
- package/cli.js +3 -0
- package/cli.ts +71 -0
- package/eslint.config.js +19 -0
- package/index.ts +109 -0
- package/package.json +31 -0
- package/src/config.ts +46 -0
- package/src/monitor.ts +236 -0
- package/src/reporter.ts +181 -0
- package/tsconfig.json +30 -0
package/.env.example
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Server Monitoring Daemon — Telegram Bot Config
|
|
2
|
+
# Copy to .env: cp .env.example .env
|
|
3
|
+
#
|
|
4
|
+
# 1. Bikin bot via @BotFather → dapet token
|
|
5
|
+
# 2. Dapetin chat ID: DM @userinfobot, atau cek getUpdates API
|
|
6
|
+
# - DM/private: 123456789
|
|
7
|
+
# - Supergroup: -1001234567890
|
|
8
|
+
|
|
9
|
+
TELEGRAM_BOT_TOKEN=your_bot_token_here
|
|
10
|
+
TELEGRAM_CHAT_ID=your_chat_id_here
|
|
11
|
+
|
|
12
|
+
# Interval monitoring dalam detik (default: 300 = 5 menit)
|
|
13
|
+
# Minimal 30 detik
|
|
14
|
+
MONITOR_INTERVAL=300
|
package/.prettierignore
ADDED
package/.prettierrc
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# 🖥 Server Monitor
|
|
2
|
+
|
|
3
|
+
> **Lightweight server watchdog** — collects real-time system metrics and sends structured reports to Telegram.
|
|
4
|
+
> Global CLI tool. Install once, run anywhere. Built with Bun + TypeScript.
|
|
5
|
+
|
|
6
|
+
<p align="center">
|
|
7
|
+
<img src="https://img.shields.io/badge/runtime-Bun-000?style=flat&logo=bun" alt="bun">
|
|
8
|
+
<img src="https://img.shields.io/badge/language-TypeScript-3178C6?style=flat&logo=typescript" alt="ts">
|
|
9
|
+
<img src="https://img.shields.io/badge/target-Telegram-26A5E4?style=flat&logo=telegram" alt="telegram">
|
|
10
|
+
<img src="https://img.shields.io/badge/license-MIT-green?style=flat" alt="license">
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## ✨ Features
|
|
16
|
+
|
|
17
|
+
| Category | What it tracks |
|
|
18
|
+
| -------------------- | -------------------------------------------------------------- |
|
|
19
|
+
| 💻 **CPU** | Model, core count, usage %, load average (1/5/15 min) |
|
|
20
|
+
| 🧠 **RAM** | Used / total, usage %, swap usage |
|
|
21
|
+
| 💾 **Disk** | Per-mount used / total, usage % — deduplicated |
|
|
22
|
+
| 🌐 **Network** | RX / TX rate (bytes/sec, sampled over 1s) |
|
|
23
|
+
| 🌡 **Temperature** | CPU package temp via thermal zones (`/sys/class/thermal`) |
|
|
24
|
+
| 📊 **Top Processes** | Top 5 by CPU % — PID, name, CPU %, MEM % |
|
|
25
|
+
| 🚨 **Alerts** | Auto-flag when CPU > 85%, RAM > 90%, disk > 90%, or swap > 50% |
|
|
26
|
+
|
|
27
|
+
Each report is color-coded with 🟢🟡🔴 health indicators and visual bar charts.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 🚀 Quick Start
|
|
32
|
+
|
|
33
|
+
### Install globally
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
bun i -g github:irsyadulibad/servermon
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### First run — interactive setup
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
servermon
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
You'll be prompted for:
|
|
46
|
+
|
|
47
|
+
1. **Telegram Bot Token** — get one from [@BotFather](https://t.me/BotFather)
|
|
48
|
+
2. **Report interval** — how often to send reports (default: 300s = 5 min)
|
|
49
|
+
|
|
50
|
+
Config is saved to `~/.irsyadulibad/servermon/config.json`.
|
|
51
|
+
|
|
52
|
+
### Send a message to your bot
|
|
53
|
+
|
|
54
|
+
DM your bot **once** (any message). The daemon auto-detects your chat ID.
|
|
55
|
+
|
|
56
|
+
### Start monitoring
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
servermon
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
That's it. The daemon auto-detects your chat ID, sends an initial report, then loops.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 🛠 Development
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
git clone https://github.com/irsyadlab/server-monitoring.git
|
|
70
|
+
cd server-monitoring
|
|
71
|
+
bun install
|
|
72
|
+
|
|
73
|
+
# Run in dev mode
|
|
74
|
+
bun start
|
|
75
|
+
|
|
76
|
+
# Global link (for testing)
|
|
77
|
+
bun link
|
|
78
|
+
servermon
|
|
79
|
+
|
|
80
|
+
# Unlink
|
|
81
|
+
bun unlink
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 📷 Example Report
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
✅ HEALTHY
|
|
90
|
+
📅 7 Jun 2026, 22:11 │ ⏱ 3d 21h
|
|
91
|
+
🐧 linux x86_64 │ Intel Xeon E5-2680 v4 @ 2.40GHz (2c)
|
|
92
|
+
|
|
93
|
+
💻 CPU 10.5% 🟢 ▰▱▱▱▱▱▱▱▱▱
|
|
94
|
+
Load: 0.59 / 1.40 / 1.51
|
|
95
|
+
|
|
96
|
+
🧠 RAM 27.3% 🟢 ▰▰▰▱▱▱▱▱▱▱
|
|
97
|
+
1.0 GiB / 3.8 GiB
|
|
98
|
+
|
|
99
|
+
💾 DISK
|
|
100
|
+
/ 26% 🟢 ▰▰▰▱▱▱▱▱▱▱
|
|
101
|
+
└ 7957 GiB / 33092 GiB
|
|
102
|
+
|
|
103
|
+
🌐 NET
|
|
104
|
+
↓ 2.6 KB/s ↑ 808 B/s
|
|
105
|
+
|
|
106
|
+
📊 TOP PROCESSES
|
|
107
|
+
57318 ps CPU 200.0% MEM 0.1%
|
|
108
|
+
57296 bun CPU 13.6% MEM 1.0%
|
|
109
|
+
56149 hermes CPU 8.7% MEM 5.3%
|
|
110
|
+
|
|
111
|
+
✨ All systems normal
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
When thresholds are crossed, alerts appear inline.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 🔧 Scripts
|
|
119
|
+
|
|
120
|
+
| Command | Description |
|
|
121
|
+
| ---------------- | ---------------------------------------------- |
|
|
122
|
+
| `bun start` | Run the daemon in dev mode |
|
|
123
|
+
| `bun run build` | Compile standalone binary → `./server-monitor` |
|
|
124
|
+
| `bun run lint` | Run ESLint |
|
|
125
|
+
| `bun run format` | Format with Prettier |
|
|
126
|
+
| `bun run check` | Format check + lint (CI-ready) |
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 📁 Project Structure
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
server-monitoring/
|
|
134
|
+
├── cli.ts # Global binary entry (interactive setup + launcher)
|
|
135
|
+
├── index.ts # Daemon core (start/loop/report)
|
|
136
|
+
├── src/
|
|
137
|
+
│ ├── config.ts # Config manager (~/.irsyadulibad/servermon/)
|
|
138
|
+
│ ├── monitor.ts # Metrics collector (CPU, RAM, disk, net, temp, procs)
|
|
139
|
+
│ └── reporter.ts # HTML formatter & Telegram sender
|
|
140
|
+
├── eslint.config.js
|
|
141
|
+
├── .prettierrc
|
|
142
|
+
├── package.json
|
|
143
|
+
└── tsconfig.json
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## ⚙️ Config
|
|
149
|
+
|
|
150
|
+
Stored at `~/.irsyadulibad/servermon/config.json`:
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"token": "883280...",
|
|
155
|
+
"interval": 300,
|
|
156
|
+
"chatId": "1216431846"
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
- `token` — Telegram bot token (required)
|
|
161
|
+
- `interval` — seconds between reports, min: 30 (default: 300)
|
|
162
|
+
- `chatId` — auto-detected on first run, persisted for subsequent runs
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## 📦 Deploy as systemd service
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
# After installing globally
|
|
170
|
+
sudo tee /etc/systemd/system/servermon.service << 'EOF'
|
|
171
|
+
[Unit]
|
|
172
|
+
Description=Server Monitor Daemon
|
|
173
|
+
After=network.target
|
|
174
|
+
|
|
175
|
+
[Service]
|
|
176
|
+
Type=simple
|
|
177
|
+
ExecStart=/root/.bun/bin/servermon
|
|
178
|
+
Restart=always
|
|
179
|
+
RestartSec=10
|
|
180
|
+
|
|
181
|
+
[Install]
|
|
182
|
+
WantedBy=multi-user.target
|
|
183
|
+
EOF
|
|
184
|
+
|
|
185
|
+
sudo systemctl daemon-reload
|
|
186
|
+
sudo systemctl enable --now servermon
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## 🛠 Built With
|
|
192
|
+
|
|
193
|
+
- [Bun](https://bun.com) — fast all-in-one JavaScript runtime
|
|
194
|
+
- [TypeScript](https://www.typescriptlang.org/) — type safety
|
|
195
|
+
- [Telegram Bot API](https://core.telegram.org/bots/api) — message delivery
|
|
196
|
+
- [ESLint](https://eslint.org/) + [Prettier](https://prettier.io/) — code quality
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## 📝 License
|
|
201
|
+
|
|
202
|
+
MIT — do whatever you want.
|
package/bun.lock
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "server-monitoring",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@eslint/js": "^10.0.1",
|
|
9
|
+
"@types/bun": "latest",
|
|
10
|
+
"eslint": "^10.4.1",
|
|
11
|
+
"eslint-config-prettier": "^10.1.8",
|
|
12
|
+
"prettier": "^3.8.3",
|
|
13
|
+
"typescript-eslint": "^8.60.1",
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"typescript": "^5",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
"packages": {
|
|
21
|
+
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
|
22
|
+
|
|
23
|
+
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
|
24
|
+
|
|
25
|
+
"@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="],
|
|
26
|
+
|
|
27
|
+
"@eslint/config-helpers": ["@eslint/config-helpers@0.6.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA=="],
|
|
28
|
+
|
|
29
|
+
"@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="],
|
|
30
|
+
|
|
31
|
+
"@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="],
|
|
32
|
+
|
|
33
|
+
"@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="],
|
|
34
|
+
|
|
35
|
+
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.2", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A=="],
|
|
36
|
+
|
|
37
|
+
"@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
|
|
38
|
+
|
|
39
|
+
"@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
|
|
40
|
+
|
|
41
|
+
"@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="],
|
|
42
|
+
|
|
43
|
+
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
|
44
|
+
|
|
45
|
+
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
|
46
|
+
|
|
47
|
+
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
|
48
|
+
|
|
49
|
+
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
|
|
50
|
+
|
|
51
|
+
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
|
52
|
+
|
|
53
|
+
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
|
54
|
+
|
|
55
|
+
"@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="],
|
|
56
|
+
|
|
57
|
+
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.60.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/type-utils": "8.60.1", "@typescript-eslint/utils": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.60.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg=="],
|
|
58
|
+
|
|
59
|
+
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.60.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA=="],
|
|
60
|
+
|
|
61
|
+
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.60.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.60.1", "@typescript-eslint/types": "^8.60.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw=="],
|
|
62
|
+
|
|
63
|
+
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.60.1", "", { "dependencies": { "@typescript-eslint/types": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1" } }, "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w=="],
|
|
64
|
+
|
|
65
|
+
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.60.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA=="],
|
|
66
|
+
|
|
67
|
+
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.60.1", "", { "dependencies": { "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/utils": "8.60.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A=="],
|
|
68
|
+
|
|
69
|
+
"@typescript-eslint/types": ["@typescript-eslint/types@8.60.1", "", {}, "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w=="],
|
|
70
|
+
|
|
71
|
+
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.60.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.60.1", "@typescript-eslint/tsconfig-utils": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew=="],
|
|
72
|
+
|
|
73
|
+
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.60.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg=="],
|
|
74
|
+
|
|
75
|
+
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.60.1", "", { "dependencies": { "@typescript-eslint/types": "8.60.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag=="],
|
|
76
|
+
|
|
77
|
+
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
|
78
|
+
|
|
79
|
+
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
|
80
|
+
|
|
81
|
+
"ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
|
82
|
+
|
|
83
|
+
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
|
84
|
+
|
|
85
|
+
"brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
|
|
86
|
+
|
|
87
|
+
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
|
88
|
+
|
|
89
|
+
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
|
90
|
+
|
|
91
|
+
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
|
92
|
+
|
|
93
|
+
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
|
94
|
+
|
|
95
|
+
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
|
96
|
+
|
|
97
|
+
"eslint": ["eslint@10.4.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw=="],
|
|
98
|
+
|
|
99
|
+
"eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="],
|
|
100
|
+
|
|
101
|
+
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
|
|
102
|
+
|
|
103
|
+
"eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
|
104
|
+
|
|
105
|
+
"espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
|
|
106
|
+
|
|
107
|
+
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
|
|
108
|
+
|
|
109
|
+
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
|
110
|
+
|
|
111
|
+
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
|
112
|
+
|
|
113
|
+
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
|
114
|
+
|
|
115
|
+
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
|
116
|
+
|
|
117
|
+
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
|
118
|
+
|
|
119
|
+
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
|
120
|
+
|
|
121
|
+
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
|
122
|
+
|
|
123
|
+
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
|
124
|
+
|
|
125
|
+
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
|
126
|
+
|
|
127
|
+
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
|
128
|
+
|
|
129
|
+
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
|
|
130
|
+
|
|
131
|
+
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
|
132
|
+
|
|
133
|
+
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
|
134
|
+
|
|
135
|
+
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
|
136
|
+
|
|
137
|
+
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
|
138
|
+
|
|
139
|
+
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
|
140
|
+
|
|
141
|
+
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
|
142
|
+
|
|
143
|
+
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
|
144
|
+
|
|
145
|
+
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
|
146
|
+
|
|
147
|
+
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
|
148
|
+
|
|
149
|
+
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
|
150
|
+
|
|
151
|
+
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
|
152
|
+
|
|
153
|
+
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
|
154
|
+
|
|
155
|
+
"minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
|
156
|
+
|
|
157
|
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
|
158
|
+
|
|
159
|
+
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
|
160
|
+
|
|
161
|
+
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
|
162
|
+
|
|
163
|
+
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
|
164
|
+
|
|
165
|
+
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
|
166
|
+
|
|
167
|
+
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
|
168
|
+
|
|
169
|
+
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
|
170
|
+
|
|
171
|
+
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
|
172
|
+
|
|
173
|
+
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
|
174
|
+
|
|
175
|
+
"prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
|
|
176
|
+
|
|
177
|
+
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
|
178
|
+
|
|
179
|
+
"semver": ["semver@7.8.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ=="],
|
|
180
|
+
|
|
181
|
+
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
|
182
|
+
|
|
183
|
+
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
|
184
|
+
|
|
185
|
+
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
|
186
|
+
|
|
187
|
+
"ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
|
|
188
|
+
|
|
189
|
+
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
|
190
|
+
|
|
191
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
192
|
+
|
|
193
|
+
"typescript-eslint": ["typescript-eslint@8.60.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.60.1", "@typescript-eslint/parser": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/utils": "8.60.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA=="],
|
|
194
|
+
|
|
195
|
+
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
|
196
|
+
|
|
197
|
+
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
|
198
|
+
|
|
199
|
+
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
|
200
|
+
|
|
201
|
+
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
|
202
|
+
|
|
203
|
+
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
|
204
|
+
|
|
205
|
+
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
|
206
|
+
|
|
207
|
+
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
|
208
|
+
}
|
|
209
|
+
}
|
package/cli.js
ADDED
package/cli.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Server Monitor — Global CLI entry point.
|
|
4
|
+
* Handles first-time interactive setup and starts the monitoring daemon.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { loadConfig, saveConfig, configPath, configDir } from "./src/config";
|
|
8
|
+
|
|
9
|
+
function banner() {
|
|
10
|
+
console.log("╔══════════════════════════════════════╗");
|
|
11
|
+
console.log("║ 🖥 SERVER MONITOR DAEMON 🖥 ║");
|
|
12
|
+
console.log("║ Telegram • Bun • TypeScript ║");
|
|
13
|
+
console.log("╚══════════════════════════════════════╝");
|
|
14
|
+
console.log();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function interactiveSetup(): Promise<void> {
|
|
18
|
+
console.log("🖥 Server Monitor — First Time Setup");
|
|
19
|
+
console.log(` Config will be saved to: ${configDir()}\n`);
|
|
20
|
+
|
|
21
|
+
const token = prompt("🔑 Telegram Bot Token: ")?.trim();
|
|
22
|
+
if (!token) {
|
|
23
|
+
console.error("❌ Bot token is required. Get one from @BotFather on Telegram.");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!token.includes(":")) {
|
|
28
|
+
console.error("❌ Invalid bot token format. Should look like: 123456:ABC-DEF1234gh...");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const intervalRaw =
|
|
33
|
+
prompt("⏱ Report interval in seconds (default 300 = 5 min): ")?.trim() || "300";
|
|
34
|
+
const interval = Math.max(30, parseInt(intervalRaw) || 300);
|
|
35
|
+
|
|
36
|
+
await saveConfig({ token, interval });
|
|
37
|
+
console.log(`\n✅ Config saved!`);
|
|
38
|
+
console.log(` 📁 ${configPath()}`);
|
|
39
|
+
console.log(` ⏱ Interval: ${interval}s (${(interval / 60).toFixed(0)} min)`);
|
|
40
|
+
console.log(`\n📡 Next step: DM your bot once on Telegram, then re-run \`servermon\`.`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function main() {
|
|
44
|
+
banner();
|
|
45
|
+
|
|
46
|
+
const config = await loadConfig();
|
|
47
|
+
|
|
48
|
+
if (!config) {
|
|
49
|
+
await interactiveSetup();
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Push config into env for the daemon
|
|
54
|
+
process.env["TELEGRAM_BOT_TOKEN"] = config.token;
|
|
55
|
+
process.env["MONITOR_INTERVAL"] = String(config.interval);
|
|
56
|
+
if (config.chatId) process.env["TELEGRAM_CHAT_ID"] = config.chatId;
|
|
57
|
+
|
|
58
|
+
console.log(`📁 Config: ${configPath()}`);
|
|
59
|
+
console.log(`📡 Bot: ...${config.token.slice(-8)}`);
|
|
60
|
+
if (config.chatId) console.log(`💬 Chat: ${config.chatId}`);
|
|
61
|
+
console.log();
|
|
62
|
+
|
|
63
|
+
// Start the daemon
|
|
64
|
+
const { start } = await import("./index.ts");
|
|
65
|
+
await start();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
main().catch((err) => {
|
|
69
|
+
console.error("❌ Fatal error:", err?.message ?? err);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
});
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import js from "@eslint/js";
|
|
2
|
+
import tseslint from "typescript-eslint";
|
|
3
|
+
import prettier from "eslint-config-prettier";
|
|
4
|
+
|
|
5
|
+
export default tseslint.config(
|
|
6
|
+
js.configs.recommended,
|
|
7
|
+
...tseslint.configs.recommended,
|
|
8
|
+
prettier,
|
|
9
|
+
{
|
|
10
|
+
ignores: ["node_modules/", "server-monitor", "*.js"],
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
rules: {
|
|
14
|
+
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
|
|
15
|
+
"@typescript-eslint/no-explicit-any": "warn",
|
|
16
|
+
"no-console": "off",
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
);
|
package/index.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { sendReport } from "./src/reporter";
|
|
2
|
+
import { saveConfig } from "./src/config";
|
|
3
|
+
|
|
4
|
+
const botToken = process.env["TELEGRAM_BOT_TOKEN"] ?? "";
|
|
5
|
+
let chatId = process.env["TELEGRAM_CHAT_ID"] ?? "";
|
|
6
|
+
const rawInterval = process.env["MONITOR_INTERVAL"] ?? "300";
|
|
7
|
+
const intervalSec = Math.max(30, parseInt(rawInterval) || 300);
|
|
8
|
+
|
|
9
|
+
// --- Auto-detect chat ID ---
|
|
10
|
+
async function autoDetectChatId(token: string): Promise<string | null> {
|
|
11
|
+
try {
|
|
12
|
+
const resp = await fetch(`https://api.telegram.org/bot${token}/getUpdates?limit=5`);
|
|
13
|
+
const data = (await resp.json()) as {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
result?: Array<{
|
|
16
|
+
message?: { chat?: { id: number; title?: string; first_name?: string } };
|
|
17
|
+
channel_post?: { chat?: { id: number; title?: string } };
|
|
18
|
+
}>;
|
|
19
|
+
};
|
|
20
|
+
if (!data.ok || !data.result?.length) return null;
|
|
21
|
+
|
|
22
|
+
const chatIds = new Set<string>();
|
|
23
|
+
for (const update of data.result.reverse()) {
|
|
24
|
+
const chat = update.message?.chat || update.channel_post?.chat;
|
|
25
|
+
if (chat?.id) chatIds.add(String(chat.id));
|
|
26
|
+
}
|
|
27
|
+
if (chatIds.size === 0) return null;
|
|
28
|
+
|
|
29
|
+
const id = [...chatIds][0]!;
|
|
30
|
+
const chatInfo = data.result.find(
|
|
31
|
+
(u) => String(u.message?.chat?.id || u.channel_post?.chat?.id) === id
|
|
32
|
+
);
|
|
33
|
+
const chatName =
|
|
34
|
+
chatInfo?.message?.chat?.title ||
|
|
35
|
+
chatInfo?.message?.chat?.first_name ||
|
|
36
|
+
chatInfo?.channel_post?.chat?.title ||
|
|
37
|
+
"Unknown";
|
|
38
|
+
console.log(`🔍 Auto-detected chat: ${chatName} (ID: ${id})`);
|
|
39
|
+
return id;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// --- Daemon ---
|
|
46
|
+
export async function start() {
|
|
47
|
+
if (!botToken) {
|
|
48
|
+
console.error("❌ TELEGRAM_BOT_TOKEN not set. Run `servermon` first to configure.");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!chatId) {
|
|
53
|
+
console.log("🔍 TELEGRAM_CHAT_ID not set — auto-detecting...");
|
|
54
|
+
const detected = await autoDetectChatId(botToken);
|
|
55
|
+
if (!detected) {
|
|
56
|
+
console.error("❌ No recent chats found. DM your bot first, then re-run.");
|
|
57
|
+
console.error(" Or set TELEGRAM_CHAT_ID in ~/.irsyadulibad/servermon/config.json");
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
chatId = detected;
|
|
61
|
+
// Persist to config
|
|
62
|
+
try {
|
|
63
|
+
const { loadConfig } = await import("./src/config");
|
|
64
|
+
const cfg = await loadConfig();
|
|
65
|
+
if (cfg) {
|
|
66
|
+
cfg.chatId = chatId;
|
|
67
|
+
await saveConfig(cfg);
|
|
68
|
+
console.log("💾 Chat ID saved to config");
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
// ok
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log(`⏱ Interval: ${intervalSec}s (${(intervalSec / 60).toFixed(0)} menit)`);
|
|
76
|
+
console.log(`📡 Bot: ...${botToken.slice(-8)}`);
|
|
77
|
+
console.log(`💬 Chat: ${chatId}`);
|
|
78
|
+
console.log();
|
|
79
|
+
|
|
80
|
+
async function tick() {
|
|
81
|
+
const start2 = Date.now();
|
|
82
|
+
const ok = await sendReport(botToken, chatId);
|
|
83
|
+
const elapsed = Date.now() - start2;
|
|
84
|
+
const ts = new Date().toLocaleString("id-ID", { timeZone: "Asia/Jakarta" });
|
|
85
|
+
console.log(`[${ts}] ${ok ? "✅" : "❌"} ${elapsed}ms`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Run once at startup
|
|
89
|
+
await tick();
|
|
90
|
+
|
|
91
|
+
// Loop
|
|
92
|
+
setInterval(tick, intervalSec * 1000);
|
|
93
|
+
|
|
94
|
+
process.on("SIGINT", () => {
|
|
95
|
+
console.log("\n👋 Shutting down...");
|
|
96
|
+
process.exit(0);
|
|
97
|
+
});
|
|
98
|
+
process.on("SIGTERM", () => {
|
|
99
|
+
console.log("\n👋 Shutting down...");
|
|
100
|
+
process.exit(0);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Direct run support (for dev / bun index.ts)
|
|
105
|
+
// When imported by cli.ts, cli.ts calls start() explicitly
|
|
106
|
+
const isDirectlyRun = import.meta.url.endsWith(process.argv[1]?.replace(/^.*\//, "") ?? "");
|
|
107
|
+
if (isDirectlyRun || process.argv[1]?.endsWith("index.ts")) {
|
|
108
|
+
start();
|
|
109
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@irsyadulibad/servermon",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Lightweight server monitoring daemon — collects system metrics and sends structured reports to Telegram. Built with Bun + TypeScript.",
|
|
5
|
+
"module": "index.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"servermon": "./cli.js"
|
|
9
|
+
},
|
|
10
|
+
"private": false,
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "bun index.ts",
|
|
13
|
+
"build": "bun build --compile index.ts --outfile server-monitor",
|
|
14
|
+
"lint": "eslint .",
|
|
15
|
+
"lint:fix": "eslint . --fix",
|
|
16
|
+
"format": "prettier --write .",
|
|
17
|
+
"format:check": "prettier --check .",
|
|
18
|
+
"check": "bun run format:check && bun run lint"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@eslint/js": "^10",
|
|
22
|
+
"@types/bun": "latest",
|
|
23
|
+
"eslint": "^10",
|
|
24
|
+
"eslint-config-prettier": "^10",
|
|
25
|
+
"prettier": "^3",
|
|
26
|
+
"typescript-eslint": "^8"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"typescript": "^5"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
}
|
package/src/monitor.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import * as os from "os";
|
|
2
|
+
|
|
3
|
+
export interface CPUMetrics {
|
|
4
|
+
model: string;
|
|
5
|
+
cores: number;
|
|
6
|
+
loadAvg: { "1min": number; "5min": number; "15min": number };
|
|
7
|
+
usagePercent: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
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
|
+
export interface DiskInfo {
|
|
21
|
+
mount: string;
|
|
22
|
+
total: number;
|
|
23
|
+
used: number;
|
|
24
|
+
available: number;
|
|
25
|
+
usagePercent: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface NetworkMetrics {
|
|
29
|
+
rxRate: number; // bytes/sec
|
|
30
|
+
txRate: number;
|
|
31
|
+
rxTotal: number;
|
|
32
|
+
txTotal: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ProcInfo {
|
|
36
|
+
pid: number;
|
|
37
|
+
name: string;
|
|
38
|
+
cpuPercent: number;
|
|
39
|
+
memPercent: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
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
|
+
}
|
|
54
|
+
|
|
55
|
+
async function exec(cmd: string): Promise<string> {
|
|
56
|
+
const proc = Bun.spawn(["bash", "-c", cmd], { stdout: "pipe" });
|
|
57
|
+
const text = await new Response(proc.stdout).text();
|
|
58
|
+
await proc.exited;
|
|
59
|
+
return text.trim();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseDfLine(line: string): DiskInfo | null {
|
|
63
|
+
const parts = line.trim().split(/\s+/);
|
|
64
|
+
if (parts.length < 6) return null;
|
|
65
|
+
const total = parseInt(parts[1]!) * 1024;
|
|
66
|
+
const used = parseInt(parts[2]!) * 1024;
|
|
67
|
+
const available = parseInt(parts[3]!) * 1024;
|
|
68
|
+
const usagePercent = parseInt(parts[4]!);
|
|
69
|
+
const mount = parts[5]!;
|
|
70
|
+
return { mount, total, used, available, usagePercent };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function readNetDev(): Promise<{ rx: number; tx: number }> {
|
|
74
|
+
const data = await exec("cat /proc/net/dev 2>/dev/null");
|
|
75
|
+
let rx = 0,
|
|
76
|
+
tx = 0;
|
|
77
|
+
for (const line of data.split("\n")) {
|
|
78
|
+
if (!line.includes(":")) continue;
|
|
79
|
+
const ifname = line.split(":")[0]!.trim();
|
|
80
|
+
// Skip loopback
|
|
81
|
+
if (ifname === "lo") continue;
|
|
82
|
+
// Skip veth, docker
|
|
83
|
+
if (ifname.startsWith("veth") || ifname.startsWith("docker") || ifname.startsWith("br-"))
|
|
84
|
+
continue;
|
|
85
|
+
const parts = line.split(":")[1]!.trim().split(/\s+/);
|
|
86
|
+
rx += parseInt(parts[0]!) || 0;
|
|
87
|
+
tx += parseInt(parts[8]!) || 0;
|
|
88
|
+
}
|
|
89
|
+
return { rx, tx };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function readTemperature(): Promise<number | null> {
|
|
93
|
+
try {
|
|
94
|
+
const zones = await exec(
|
|
95
|
+
`for z in /sys/class/thermal/thermal_zone*/temp; do [ -r "$z" ] && echo "$z=$(cat "$z")"; done 2>/dev/null`
|
|
96
|
+
);
|
|
97
|
+
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
|
+
let highest = -Infinity;
|
|
107
|
+
for (const line of zones.split("\n")) {
|
|
108
|
+
const val = parseInt(line.split("=")[1]!);
|
|
109
|
+
if (val > 0 && val > highest) highest = val;
|
|
110
|
+
}
|
|
111
|
+
return highest > 0 ? highest / 1000 : null;
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function collectMetrics(): Promise<SystemMetrics> {
|
|
118
|
+
// --- disk ---
|
|
119
|
+
const dfOutput = await exec("df -P -B1 / /home 2>/dev/null");
|
|
120
|
+
const disks: DiskInfo[] = dfOutput
|
|
121
|
+
.split("\n")
|
|
122
|
+
.slice(1)
|
|
123
|
+
.map(parseDfLine)
|
|
124
|
+
.filter(Boolean)
|
|
125
|
+
.filter((d, i, arr) => arr.findIndex((x) => x.mount === d.mount) === i) as DiskInfo[];
|
|
126
|
+
|
|
127
|
+
// --- memory ---
|
|
128
|
+
const memOutput = await exec("free -b 2>/dev/null");
|
|
129
|
+
const memLines = memOutput.split("\n");
|
|
130
|
+
const memParts = memLines[1]?.trim().split(/\s+/);
|
|
131
|
+
const swapParts = memLines[2]?.trim().split(/\s+/);
|
|
132
|
+
const memTotal = memParts ? parseInt(memParts[1]!) : os.totalmem();
|
|
133
|
+
const memUsed = memParts ? parseInt(memParts[2]!) : os.totalmem() - os.freemem();
|
|
134
|
+
const memFree = memParts ? parseInt(memParts[3]!) : os.freemem();
|
|
135
|
+
const swapTotal = swapParts ? parseInt(swapParts[1]!) : 0;
|
|
136
|
+
const swapUsed = swapParts ? parseInt(swapParts[2]!) : 0;
|
|
137
|
+
|
|
138
|
+
// --- CPU usage (poll /proc/stat with 500ms delay) ---
|
|
139
|
+
const loadAvg = os.loadavg();
|
|
140
|
+
const stat1 = await exec("cat /proc/stat | grep '^cpu '");
|
|
141
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
142
|
+
const stat2 = await exec("cat /proc/stat | grep '^cpu '");
|
|
143
|
+
|
|
144
|
+
function cpuTicks(stat: string): number[] {
|
|
145
|
+
return stat.split(/\s+/).slice(1).map(Number);
|
|
146
|
+
}
|
|
147
|
+
const t1 = cpuTicks(stat1);
|
|
148
|
+
const t2 = cpuTicks(stat2);
|
|
149
|
+
let usagePercent = 0;
|
|
150
|
+
if (t1.length >= 4 && t2.length >= 4) {
|
|
151
|
+
const idle1 = t1[3]! + (t1[4] ?? 0);
|
|
152
|
+
const idle2 = t2[3]! + (t2[4] ?? 0);
|
|
153
|
+
const total1 = t1.reduce((a, b) => a + b, 0);
|
|
154
|
+
const total2 = t2.reduce((a, b) => a + b, 0);
|
|
155
|
+
const totalDelta = total2 - total1;
|
|
156
|
+
const idleDelta = idle2 - idle1;
|
|
157
|
+
if (totalDelta > 0) usagePercent = ((totalDelta - idleDelta) / totalDelta) * 100;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// --- network rate (poll 1s delta) ---
|
|
161
|
+
const net1 = await readNetDev();
|
|
162
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
163
|
+
const net2 = await readNetDev();
|
|
164
|
+
const rxRate = Math.max(0, net2.rx - net1.rx); // bytes/sec
|
|
165
|
+
const txRate = Math.max(0, net2.tx - net1.tx);
|
|
166
|
+
|
|
167
|
+
// --- top processes ---
|
|
168
|
+
const topOutput = await exec(
|
|
169
|
+
"ps -eo pid,comm,pcpu,pmem --sort=-pcpu --no-headers 2>/dev/null | head -5"
|
|
170
|
+
);
|
|
171
|
+
const topProcs: ProcInfo[] = topOutput
|
|
172
|
+
.split("\n")
|
|
173
|
+
.filter(Boolean)
|
|
174
|
+
.map((line) => {
|
|
175
|
+
const p = line.trim().split(/\s+/);
|
|
176
|
+
return {
|
|
177
|
+
pid: parseInt(p[0]!) || 0,
|
|
178
|
+
name: p[1]?.slice(0, 15) ?? "?",
|
|
179
|
+
cpuPercent: parseFloat(p[2]!) || 0,
|
|
180
|
+
memPercent: parseFloat(p[3]!) || 0,
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// --- temperature ---
|
|
185
|
+
const temperature = await readTemperature();
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
hostname: os.hostname(),
|
|
189
|
+
platform: os.platform(),
|
|
190
|
+
arch: os.machine(),
|
|
191
|
+
uptime: os.uptime(),
|
|
192
|
+
cpu: {
|
|
193
|
+
model: (os.cpus()[0]?.model ?? "unknown").trim(),
|
|
194
|
+
cores: os.cpus().length,
|
|
195
|
+
loadAvg: { "1min": loadAvg[0]!, "5min": loadAvg[1]!, "15min": loadAvg[2]! },
|
|
196
|
+
usagePercent: Math.round(usagePercent * 10) / 10,
|
|
197
|
+
},
|
|
198
|
+
memory: {
|
|
199
|
+
total: memTotal,
|
|
200
|
+
used: memUsed,
|
|
201
|
+
free: memFree,
|
|
202
|
+
usagePercent: Math.round((memUsed / memTotal) * 1000) / 10,
|
|
203
|
+
swapTotal,
|
|
204
|
+
swapUsed,
|
|
205
|
+
swapUsagePercent: swapTotal > 0 ? Math.round((swapUsed / swapTotal) * 1000) / 10 : 0,
|
|
206
|
+
},
|
|
207
|
+
disks,
|
|
208
|
+
network: { rxRate, txRate, rxTotal: net2.rx, txTotal: net2.tx },
|
|
209
|
+
topProcs,
|
|
210
|
+
temperature,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
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
|
+
}
|
package/src/reporter.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { formatBytes, formatRate, formatUptime, type SystemMetrics } from "./monitor";
|
|
2
|
+
|
|
3
|
+
// HTML escape
|
|
4
|
+
function esc(s: string): string {
|
|
5
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Bar chart with inline colored blocks
|
|
9
|
+
function bar(percent: number, w = 10): string {
|
|
10
|
+
const filled = Math.min(w, Math.max(0, Math.round((percent / 100) * w)));
|
|
11
|
+
const empty = w - filled;
|
|
12
|
+
const color = percent > 80 ? "🔴" : percent > 50 ? "🟡" : "🟢";
|
|
13
|
+
return color + " " + "▰".repeat(filled) + "▱".repeat(empty);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function pad(n: number, dp = 1): string {
|
|
17
|
+
return n.toFixed(dp).padStart(5);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Overall health
|
|
21
|
+
function healthTag(m: SystemMetrics): string {
|
|
22
|
+
const d = m.disks.length ? Math.max(...m.disks.map((x) => x.usagePercent)) : 0;
|
|
23
|
+
if (m.cpu.usagePercent > 85 || m.memory.usagePercent > 95 || d > 95) return "🚨 CRITICAL";
|
|
24
|
+
if (m.cpu.usagePercent > 70 || m.memory.usagePercent > 85 || d > 85) return "⚠️ WARNING";
|
|
25
|
+
return "✅ HEALTHY";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function formatReportHTML(m: SystemMetrics): string {
|
|
29
|
+
const now = new Date().toLocaleString("id-ID", {
|
|
30
|
+
timeZone: "Asia/Jakarta",
|
|
31
|
+
day: "numeric",
|
|
32
|
+
month: "short",
|
|
33
|
+
year: "numeric",
|
|
34
|
+
hour: "2-digit",
|
|
35
|
+
minute: "2-digit",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const tempStr = m.temperature !== null ? ` 🌡 ${m.temperature.toFixed(0)}°C` : "";
|
|
39
|
+
|
|
40
|
+
// ── Header ──
|
|
41
|
+
const header = [
|
|
42
|
+
`<b>🖥 ${esc(m.hostname)}</b> — ${healthTag(m)}`,
|
|
43
|
+
`📅 ${esc(now)} │ ⏱ ${esc(formatUptime(m.uptime))}${tempStr}`,
|
|
44
|
+
`🐧 ${esc(m.platform)} ${esc(m.arch)} │ ${esc(m.cpu.model)} (${m.cpu.cores}c)`,
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// ── CPU card ──
|
|
48
|
+
const cpu = [
|
|
49
|
+
`<b>💻 CPU</b> <code>${pad(m.cpu.usagePercent)}%</code> ${bar(m.cpu.usagePercent)}`,
|
|
50
|
+
` 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
|
+
];
|
|
52
|
+
|
|
53
|
+
// ── Memory card ──
|
|
54
|
+
const mem = [
|
|
55
|
+
`<b>🧠 RAM</b> <code>${pad(m.memory.usagePercent)}%</code> ${bar(m.memory.usagePercent)}`,
|
|
56
|
+
` <code>${esc(formatBytes(m.memory.used))}</code> / <code>${esc(formatBytes(m.memory.total))}</code>`,
|
|
57
|
+
m.memory.swapTotal > 0
|
|
58
|
+
? ` Swap: <code>${esc(formatBytes(m.memory.swapUsed))}</code> / <code>${esc(formatBytes(m.memory.swapTotal))}</code> (${m.memory.swapUsagePercent.toFixed(1)}%)`
|
|
59
|
+
: "",
|
|
60
|
+
].filter(Boolean);
|
|
61
|
+
|
|
62
|
+
// ── Disk card ──
|
|
63
|
+
const disks = [`<b>💾 DISK</b>`];
|
|
64
|
+
for (const d of m.disks) {
|
|
65
|
+
disks.push(
|
|
66
|
+
` <code>${esc(d.mount)}</code> <code>${pad(d.usagePercent, 0)}%</code> ${bar(d.usagePercent)}`
|
|
67
|
+
);
|
|
68
|
+
disks.push(
|
|
69
|
+
` └ <code>${esc(formatBytes(d.used))}</code> / <code>${esc(formatBytes(d.total))}</code>`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Network card ──
|
|
74
|
+
const net = [
|
|
75
|
+
`<b>🌐 NET</b>`,
|
|
76
|
+
` ↓ <code>${esc(formatRate(m.network.rxRate))}</code> ↑ <code>${esc(formatRate(m.network.txRate))}</code>`,
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
// ── Top processes ──
|
|
80
|
+
const procLines: string[] = [];
|
|
81
|
+
if (m.topProcs.length > 0) {
|
|
82
|
+
procLines.push(`<b>📊 TOP PROCESSES</b>`);
|
|
83
|
+
for (const p of m.topProcs) {
|
|
84
|
+
procLines.push(
|
|
85
|
+
` <code>${String(p.pid).padStart(6)}</code> ${esc(p.name.slice(0, 15).padEnd(15))} CPU <code>${p.cpuPercent.toFixed(1).padStart(5)}%</code> MEM <code>${p.memPercent.toFixed(1).padStart(5)}%</code>`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Alerts ──
|
|
91
|
+
const alerts: string[] = [];
|
|
92
|
+
if (m.cpu.usagePercent > 85) alerts.push(`🔴 CPU tinggi: ${m.cpu.usagePercent.toFixed(1)}%`);
|
|
93
|
+
if (m.memory.usagePercent > 90)
|
|
94
|
+
alerts.push(`🔴 RAM hampir penuh: ${m.memory.usagePercent.toFixed(1)}%`);
|
|
95
|
+
if (m.memory.swapUsagePercent > 50)
|
|
96
|
+
alerts.push(`🟡 Swap tinggi: ${m.memory.swapUsagePercent.toFixed(1)}%`);
|
|
97
|
+
for (const d of m.disks) {
|
|
98
|
+
if (d.usagePercent > 90)
|
|
99
|
+
alerts.push(`🔴 Disk <code>${esc(d.mount)}</code>: ${d.usagePercent}%`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (alerts.length > 0) {
|
|
103
|
+
alerts.unshift(`<b>⚠️ ALERTS</b>`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return [
|
|
107
|
+
...header,
|
|
108
|
+
"",
|
|
109
|
+
...cpu,
|
|
110
|
+
"",
|
|
111
|
+
...mem,
|
|
112
|
+
"",
|
|
113
|
+
...disks,
|
|
114
|
+
"",
|
|
115
|
+
...net,
|
|
116
|
+
...(procLines.length ? ["", ...procLines] : []),
|
|
117
|
+
...(alerts.length ? ["", ...alerts] : []),
|
|
118
|
+
"",
|
|
119
|
+
alerts.length === 0 ? "✨ All systems normal" : "",
|
|
120
|
+
]
|
|
121
|
+
.filter(Boolean)
|
|
122
|
+
.join("\n");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function sendMessage(botToken: string, chatId: string, text: string): Promise<Response> {
|
|
126
|
+
return fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: { "Content-Type": "application/json" },
|
|
129
|
+
body: JSON.stringify({
|
|
130
|
+
chat_id: chatId,
|
|
131
|
+
text,
|
|
132
|
+
parse_mode: "HTML",
|
|
133
|
+
disable_web_page_preview: true,
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function sendReport(botToken: string, chatId: string): Promise<boolean> {
|
|
139
|
+
if (!botToken || !chatId) {
|
|
140
|
+
console.error("❌ TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID are required.");
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const m = await import("./monitor").then((x) => x.collectMetrics());
|
|
145
|
+
const report = formatReportHTML(m);
|
|
146
|
+
|
|
147
|
+
// Telegram 4096 char limit — split on double newlines
|
|
148
|
+
if (report.length > 4000) {
|
|
149
|
+
const chunks = report.split("\n\n");
|
|
150
|
+
let current = "";
|
|
151
|
+
for (const chunk of chunks) {
|
|
152
|
+
if (current.length + chunk.length + 2 > 4000) {
|
|
153
|
+
const r = await sendMessage(botToken, chatId, current);
|
|
154
|
+
if (!r.ok) {
|
|
155
|
+
console.error(`❌ Telegram: ${r.status} ${await r.text()}`);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
current = chunk;
|
|
159
|
+
} else {
|
|
160
|
+
current += (current ? "\n\n" : "") + chunk;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (current) {
|
|
164
|
+
const r = await sendMessage(botToken, chatId, current);
|
|
165
|
+
if (!r.ok) {
|
|
166
|
+
console.error(`❌ Telegram: ${r.status} ${await r.text()}`);
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
console.log("📤 Report sent (chunked)");
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const resp = await sendMessage(botToken, chatId, report);
|
|
175
|
+
if (!resp.ok) {
|
|
176
|
+
console.error(`❌ Telegram: ${resp.status} ${await resp.text()}`);
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
console.log("📤 Report sent to Telegram");
|
|
180
|
+
return true;
|
|
181
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"types": ["bun"],
|
|
11
|
+
|
|
12
|
+
// Bundler mode
|
|
13
|
+
"moduleResolution": "bundler",
|
|
14
|
+
"allowImportingTsExtensions": true,
|
|
15
|
+
"verbatimModuleSyntax": true,
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
|
|
18
|
+
// Best practices
|
|
19
|
+
"strict": true,
|
|
20
|
+
"skipLibCheck": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true,
|
|
22
|
+
"noUncheckedIndexedAccess": true,
|
|
23
|
+
"noImplicitOverride": true,
|
|
24
|
+
|
|
25
|
+
// Some stricter flags (disabled by default)
|
|
26
|
+
"noUnusedLocals": false,
|
|
27
|
+
"noUnusedParameters": false,
|
|
28
|
+
"noPropertyAccessFromIndexSignature": false
|
|
29
|
+
}
|
|
30
|
+
}
|