@yourfam/yf-vitals 0.1.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/src/sample.js ADDED
@@ -0,0 +1,411 @@
1
+ import os from "node:os";
2
+ import { createWindowsDiskReader } from "./disk.js";
3
+ import { rateFromCounters } from "./rates.js";
4
+
5
+ /**
6
+ * @typedef {object} CpuSample
7
+ * @property {number} percent
8
+ *
9
+ * @typedef {object} RamSample
10
+ * @property {number} percent
11
+ * @property {number} used
12
+ * @property {number} total
13
+ *
14
+ * @typedef {object} DiskSample
15
+ * @property {number | null} readBps
16
+ * @property {number | null} writeBps
17
+ *
18
+ * @typedef {object} NetSample
19
+ * @property {number | null} txBps
20
+ * @property {number | null} rxBps
21
+ *
22
+ * @typedef {object} GpuSample
23
+ * @property {number | null} percent
24
+ * @property {number | null} used
25
+ * @property {number | null} total
26
+ *
27
+ * @typedef {object} Snapshot
28
+ * @property {number} ts
29
+ * @property {string} hostname
30
+ * @property {string} osName
31
+ * @property {number} physical
32
+ * @property {number} logical
33
+ * @property {number} ramTotal
34
+ * @property {CpuSample | null} cpu
35
+ * @property {RamSample | null} ram
36
+ * @property {DiskSample | null} disk
37
+ * @property {NetSample | null} net
38
+ * @property {GpuSample | null} gpu
39
+ */
40
+
41
+ /**
42
+ * @param {number | null | undefined} n
43
+ */
44
+ function num(n) {
45
+ const v = Number(n);
46
+ return Number.isFinite(v) ? v : null;
47
+ }
48
+
49
+ /**
50
+ * @param {import("systeminformation").OsData | null} info
51
+ */
52
+ export function shortOsName(info) {
53
+ const distro = String(info?.distro || "").trim();
54
+ if (/windows\s*11/i.test(distro) || (info == null && os.type() === "Windows_NT" && windowsLabel(os.release()) === "Windows 11")) {
55
+ return "Windows 11";
56
+ }
57
+ if (/windows\s*10/i.test(distro)) return "Windows 10";
58
+ if (/windows/i.test(distro) || os.type() === "Windows_NT") {
59
+ return windowsLabel(os.release());
60
+ }
61
+ if (!info) return os.type() || "unknown";
62
+ const release = String(info.release || "").trim();
63
+ if (distro && release && !distro.includes(release)) {
64
+ const combined = `${distro} ${release}`.trim();
65
+ if (combined.length <= 24) return combined;
66
+ }
67
+ if (distro) return distro.length <= 24 ? distro : distro.slice(0, 24);
68
+ if (info.platform) return String(info.platform);
69
+ return "unknown";
70
+ }
71
+
72
+ /**
73
+ * @param {string} release
74
+ */
75
+ function windowsLabel(release) {
76
+ const major = Number(String(release).split(".")[0]);
77
+ if (major >= 10) {
78
+ const build = Number(String(release).split(".")[2] || 0);
79
+ if (build >= 22000) return "Windows 11";
80
+ return "Windows 10";
81
+ }
82
+ return "Windows";
83
+ }
84
+
85
+ /**
86
+ * GPU memory fields in systeminformation are sometimes MiB, sometimes bytes.
87
+ * @param {number | null} n
88
+ * @param {number | null} vramMb
89
+ */
90
+ export function gpuBytes(n, vramMb) {
91
+ if (n != null && n >= 256 * 1024 * 1024) return n;
92
+ if (n != null && n > 0) return n * 1024 * 1024;
93
+ if (vramMb != null && vramMb > 0) return vramMb * 1024 * 1024;
94
+ return null;
95
+ }
96
+
97
+ /**
98
+ * @param {Array<{ utilizationGpu?: number, memoryUsed?: number, memoryTotal?: number, vram?: number }>} controllers
99
+ * @returns {GpuSample | null}
100
+ */
101
+ export function pickGpu(controllers) {
102
+ if (!Array.isArray(controllers) || controllers.length === 0) return null;
103
+ const usable = controllers.filter((c) => {
104
+ const util = num(c.utilizationGpu);
105
+ const total = gpuBytes(num(c.memoryTotal), num(c.vram));
106
+ const used = gpuBytes(num(c.memoryUsed), null);
107
+ return util != null || (total != null && total > 0) || used != null;
108
+ });
109
+ if (usable.length === 0) return null;
110
+ const c =
111
+ usable.find((x) => num(x.utilizationGpu) != null) || usable[0];
112
+ const percent = num(c.utilizationGpu);
113
+ const total = gpuBytes(num(c.memoryTotal), num(c.vram));
114
+ const used = gpuBytes(num(c.memoryUsed), null);
115
+ return {
116
+ percent,
117
+ used,
118
+ total,
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Keep last good per-row values when a metric throws or returns null.
124
+ * GPU stays null (omit row) until a usable sample appears.
125
+ *
126
+ * @param {Snapshot | null} last
127
+ * @param {Partial<Snapshot> & { ts: number }} next
128
+ * @returns {Snapshot}
129
+ */
130
+ export function mergeLastGood(last, next) {
131
+ const base = last || {
132
+ ts: next.ts,
133
+ hostname: "localhost",
134
+ osName: "unknown",
135
+ physical: 0,
136
+ logical: 0,
137
+ ramTotal: 0,
138
+ cpu: null,
139
+ ram: null,
140
+ disk: null,
141
+ net: null,
142
+ gpu: null,
143
+ };
144
+ return {
145
+ ts: next.ts,
146
+ hostname: next.hostname ?? base.hostname,
147
+ osName: next.osName ?? base.osName,
148
+ physical: next.physical ?? base.physical,
149
+ logical: next.logical ?? base.logical,
150
+ ramTotal: next.ramTotal ?? base.ramTotal,
151
+ cpu: next.cpu ?? base.cpu,
152
+ ram: next.ram ?? base.ram,
153
+ disk: mergePair(next.disk, base.disk, "readBps", "writeBps"),
154
+ net: mergePair(next.net, base.net, "txBps", "rxBps"),
155
+ gpu: next.gpu ?? base.gpu,
156
+ };
157
+ }
158
+
159
+ /**
160
+ * @param {Record<string, number | null> | null | undefined} next
161
+ * @param {Record<string, number | null> | null | undefined} last
162
+ * @param {string} a
163
+ * @param {string} b
164
+ */
165
+ function mergePair(next, last, a, b) {
166
+ if (!next && !last) return null;
167
+ if (!next) return last;
168
+ return {
169
+ [a]: next[a] ?? last?.[a] ?? null,
170
+ [b]: next[b] ?? last?.[b] ?? null,
171
+ };
172
+ }
173
+
174
+ /**
175
+ * @param {os.CpuInfo[]} cpus
176
+ */
177
+ export function cpuIdleTotal(cpus) {
178
+ let idle = 0;
179
+ let total = 0;
180
+ for (const c of cpus) {
181
+ const t = c.times;
182
+ const i = t.idle || 0;
183
+ const sum = (t.user || 0) + (t.nice || 0) + (t.sys || 0) + (t.irq || 0) + i;
184
+ idle += i;
185
+ total += sum;
186
+ }
187
+ return { idle, total };
188
+ }
189
+
190
+ /**
191
+ * @param {{ idle: number, total: number } | null} prev
192
+ * @param {{ idle: number, total: number }} curr
193
+ */
194
+ export function cpuPercentFromDelta(prev, curr) {
195
+ if (!prev) return null;
196
+ const idle = curr.idle - prev.idle;
197
+ const total = curr.total - prev.total;
198
+ if (total <= 0) return null;
199
+ const pct = (1 - idle / total) * 100;
200
+ if (!Number.isFinite(pct)) return null;
201
+ return Math.min(100, Math.max(0, pct));
202
+ }
203
+
204
+ /**
205
+ * @param {Array<{ iface?: string, rx_bytes?: number, tx_bytes?: number, operstate?: string, internal?: boolean }>} stats
206
+ */
207
+ export function sumNetBytes(stats) {
208
+ let rx = 0;
209
+ let tx = 0;
210
+ let any = false;
211
+ if (!Array.isArray(stats)) return { rx: null, tx: null };
212
+ for (const n of stats) {
213
+ const name = String(n.iface || "");
214
+ if (n.internal || name === "lo" || name === "lo0" || /^Loopback/i.test(name)) {
215
+ continue;
216
+ }
217
+ if (n.operstate && n.operstate !== "up" && n.operstate !== "unknown") {
218
+ continue;
219
+ }
220
+ const r = num(n.rx_bytes);
221
+ const t = num(n.tx_bytes);
222
+ if (r == null && t == null) continue;
223
+ any = true;
224
+ rx += r ?? 0;
225
+ tx += t ?? 0;
226
+ }
227
+ if (!any) return { rx: null, tx: null };
228
+ return { rx, tx };
229
+ }
230
+
231
+ /**
232
+ * @param {{ cpu?: Function, currentLoad?: Function, mem?: Function, osInfo?: Function, fsStats?: Function, networkStats?: Function, graphics?: Function }} [si]
233
+ */
234
+ export function createSampler(si) {
235
+ /** @type {null | { idle: number, total: number }} */
236
+ let prevCpu = null;
237
+ /** @type {null | { rx: number, wx: number }} */
238
+ let prevDisk = null;
239
+ /** @type {null | { rx: number, tx: number }} */
240
+ let prevNet = null;
241
+ /** @type {number | null} */
242
+ let prevTs = null;
243
+ /** @type {Snapshot | null} */
244
+ let last = null;
245
+ /** @type {Promise<{ hostname: string, osName: string, physical: number, logical: number, ramTotal: number }> | null} */
246
+ let staticP = null;
247
+ const winDisk = process.platform === "win32" ? createWindowsDiskReader() : null;
248
+
249
+ async function loadSi() {
250
+ if (si) return si;
251
+ const mod = await import("systeminformation");
252
+ return mod.default ?? mod;
253
+ }
254
+
255
+ async function loadStatic() {
256
+ const lib = await loadSi();
257
+ const hostname = os.hostname();
258
+ let osName = shortOsName(null);
259
+ let logical = Math.max(os.cpus().length, 1);
260
+ let physical = logical;
261
+ let ramTotal = os.totalmem();
262
+ try {
263
+ const info = await lib.osInfo();
264
+ osName = shortOsName(info);
265
+ } catch {
266
+ // keep fallback
267
+ }
268
+ try {
269
+ const cpu = await lib.cpu();
270
+ if (cpu?.physicalCores) physical = cpu.physicalCores;
271
+ if (cpu?.cores) logical = cpu.cores;
272
+ } catch {
273
+ // keep os.cpus() counts
274
+ }
275
+ try {
276
+ const mem = await lib.mem();
277
+ if (mem?.total) ramTotal = mem.total;
278
+ } catch {
279
+ // keep os.totalmem
280
+ }
281
+ return { hostname, osName, physical, logical, ramTotal };
282
+ }
283
+
284
+ async function sampleCpu(lib) {
285
+ const curr = cpuIdleTotal(os.cpus());
286
+ if (!prevCpu) {
287
+ prevCpu = curr;
288
+ await new Promise((r) => setTimeout(r, 50));
289
+ const curr2 = cpuIdleTotal(os.cpus());
290
+ const pctFast = cpuPercentFromDelta(prevCpu, curr2);
291
+ prevCpu = curr2;
292
+ if (pctFast != null) return { percent: pctFast };
293
+ } else {
294
+ const pct = cpuPercentFromDelta(prevCpu, curr);
295
+ prevCpu = curr;
296
+ if (pct != null) return { percent: pct };
297
+ }
298
+ try {
299
+ const load = await lib.currentLoad();
300
+ const pct = num(load?.currentLoad);
301
+ if (pct != null) return { percent: Math.min(100, Math.max(0, pct)) };
302
+ } catch {
303
+ // keep n/a
304
+ }
305
+ return null;
306
+ }
307
+
308
+ async function sampleRam(lib, ramTotal) {
309
+ try {
310
+ const mem = await lib.mem();
311
+ const total = num(mem?.total) ?? ramTotal;
312
+ const used = num(mem?.used) ?? num(mem?.active);
313
+ if (used == null || total == null || total <= 0) return null;
314
+ return {
315
+ percent: Math.min(100, Math.max(0, (used / total) * 100)),
316
+ used,
317
+ total,
318
+ };
319
+ } catch {
320
+ const total = ramTotal || os.totalmem();
321
+ const free = os.freemem();
322
+ const used = total - free;
323
+ if (total <= 0) return null;
324
+ return {
325
+ percent: Math.min(100, Math.max(0, (used / total) * 100)),
326
+ used,
327
+ total,
328
+ };
329
+ }
330
+ }
331
+
332
+ async function sampleDisk(lib, ts) {
333
+ let rx = null;
334
+ let wx = null;
335
+ try {
336
+ const fs = await lib.fsStats();
337
+ rx = num(fs?.rx);
338
+ wx = num(fs?.wx);
339
+ } catch {
340
+ // Windows fsStats is always null; fall through
341
+ }
342
+ if (rx == null && wx == null && winDisk) {
343
+ const w = await winDisk.read();
344
+ if (w) {
345
+ rx = w.rx;
346
+ wx = w.wx;
347
+ }
348
+ }
349
+ if (rx == null && wx == null) return { readBps: null, writeBps: null };
350
+ const readBps = rateFromCounters(prevDisk?.rx, rx ?? prevDisk?.rx ?? 0, prevTs, ts);
351
+ const writeBps = rateFromCounters(prevDisk?.wx, wx ?? prevDisk?.wx ?? 0, prevTs, ts);
352
+ prevDisk = { rx: rx ?? 0, wx: wx ?? 0 };
353
+ return { readBps, writeBps };
354
+ }
355
+
356
+ async function sampleNet(lib, ts) {
357
+ try {
358
+ const stats = await lib.networkStats("*");
359
+ const { rx, tx } = sumNetBytes(Array.isArray(stats) ? stats : stats ? [stats] : []);
360
+ const rxBps = rateFromCounters(prevNet?.rx, rx, prevTs, ts);
361
+ const txBps = rateFromCounters(prevNet?.tx, tx, prevTs, ts);
362
+ if (rx != null && tx != null) prevNet = { rx, tx };
363
+ return { txBps, rxBps };
364
+ } catch {
365
+ return { txBps: null, rxBps: null };
366
+ }
367
+ }
368
+
369
+ async function sampleGpu(lib) {
370
+ try {
371
+ const g = await lib.graphics();
372
+ return pickGpu(g?.controllers || []);
373
+ } catch {
374
+ return null;
375
+ }
376
+ }
377
+
378
+ return {
379
+ /**
380
+ * @returns {Promise<Snapshot>}
381
+ */
382
+ async sample() {
383
+ const lib = await loadSi();
384
+ if (!staticP) staticP = loadStatic();
385
+ const meta = await staticP;
386
+ const ts = Date.now();
387
+ const [cpu, ram, disk, net, gpu] = await Promise.all([
388
+ sampleCpu(lib).catch(() => last?.cpu ?? null),
389
+ sampleRam(lib, meta.ramTotal).catch(() => last?.ram ?? null),
390
+ sampleDisk(lib, ts).catch(() => last?.disk ?? { readBps: null, writeBps: null }),
391
+ sampleNet(lib, ts).catch(() => last?.net ?? { txBps: null, rxBps: null }),
392
+ sampleGpu(lib).catch(() => last?.gpu ?? null),
393
+ ]);
394
+ prevTs = ts;
395
+ const snap = mergeLastGood(last, {
396
+ ts,
397
+ ...meta,
398
+ cpu,
399
+ ram,
400
+ disk,
401
+ net,
402
+ gpu,
403
+ });
404
+ last = snap;
405
+ return snap;
406
+ },
407
+ close() {
408
+ winDisk?.close();
409
+ },
410
+ };
411
+ }
package/src/tty.js ADDED
@@ -0,0 +1,35 @@
1
+ export const ENTER_ALT = "\u001b[?1049h";
2
+ export const LEAVE_ALT = "\u001b[?1049l";
3
+ export const HIDE_CURSOR = "\u001b[?25l";
4
+ export const SHOW_CURSOR = "\u001b[?25h";
5
+ export const CLEAR_HOME = "\u001b[H\u001b[J";
6
+
7
+ /**
8
+ * @param {(s: string) => void} write
9
+ * @param {NodeJS.ReadStream | { isTTY?: boolean, setRawMode?: Function, resume?: Function } | null} [stdin]
10
+ */
11
+ export function enterTerminal(write, stdin) {
12
+ write(ENTER_ALT + HIDE_CURSOR);
13
+ if (stdin && stdin.isTTY && typeof stdin.setRawMode === "function") {
14
+ stdin.setRawMode(true);
15
+ if (typeof stdin.resume === "function") stdin.resume();
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Restore cursor, leave the alternate screen, and drop raw mode.
21
+ * Safe to call more than once.
22
+ *
23
+ * @param {(s: string) => void} write
24
+ * @param {NodeJS.ReadStream | { isTTY?: boolean, setRawMode?: Function, isRaw?: boolean } | null} [stdin]
25
+ */
26
+ export function restoreTerminal(write, stdin) {
27
+ try {
28
+ if (stdin && stdin.isTTY && typeof stdin.setRawMode === "function") {
29
+ stdin.setRawMode(false);
30
+ }
31
+ } catch {
32
+ // stdin may already be destroyed
33
+ }
34
+ write(SHOW_CURSOR + LEAVE_ALT);
35
+ }