@yourfam/yf-vitals 0.1.0 → 0.1.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.
package/README.md CHANGED
@@ -37,7 +37,7 @@ One live dashboard. Refresh every **1.0s** (or `--interval`, or **2.0s** with `-
37
37
  | **CPU** | Overall utilization 0–100%, plus physical / logical core counts |
38
38
  | **RAM** | Percent used, plus used / total (`GiB` / `MiB`) |
39
39
  | **DSK** | Read and write **rates** (KiB/s, MiB/s, GiB/s) |
40
- | **NET** | Send ↑ and receive ↓ in **Mbps** (1 Mbps = 1e6 bit/s) |
40
+ | **NET** | Send ↑ and receive ↓ in **kbps / Mbps / Gbps** (decimal bits; 1 Mbps = 1e6 bit/s) |
41
41
  | **GPU** | Utilization and VRAM when telemetry exists; **the row is omitted** when it does not |
42
42
 
43
43
  Header: `yf-vitals` · hostname · OS · `NP/NL` cores · RAM total · tick interval.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yourfam/yf-vitals",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Live terminal dashboard for CPU, RAM, disk, net, and optional GPU",
5
5
  "type": "module",
6
6
  "bin": {
package/src/args.js CHANGED
@@ -98,7 +98,7 @@ Usage:
98
98
  --version, -V Package version
99
99
 
100
100
  q / Q / Ctrl+C quit. GPU row is omitted when the OS has no GPU telemetry.
101
- Disk rates are MiB/s; network rates are Mbps.
101
+ Disk rates are KiB/s–GiB/s; network rates are kbps/Mbps/Gbps.
102
102
  Disk / net sparklines scale to the max in the last 60 ticks (no 100% ceiling).
103
103
  Needs a terminal (no pipes). Node 20+.`;
104
104
  }
package/src/disk.js CHANGED
@@ -1,23 +1,43 @@
1
1
  import { spawn } from "node:child_process";
2
2
 
3
- const PS_DISK = `
3
+ const PS_COUNTERS = `
4
4
  $ErrorActionPreference = 'SilentlyContinue'
5
5
  while (($line = [Console]::In.ReadLine()) -ne $null) {
6
6
  if ($line -eq 'q') { break }
7
+ $dr = [int64]0
8
+ $dw = [int64]0
9
+ $rx = [int64]0
10
+ $tx = [int64]0
7
11
  $d = Get-CimInstance -ClassName Win32_PerfRawData_PerfDisk_PhysicalDisk -Filter "Name='_Total'"
8
- if ($d) { Write-Output ("{0} {1}" -f $d.DiskReadBytesPersec, $d.DiskWriteBytesPersec) }
9
- else { Write-Output "n n" }
12
+ if ($d) {
13
+ $dr = [int64]$d.DiskReadBytesPersec
14
+ $dw = [int64]$d.DiskWriteBytesPersec
15
+ }
16
+ Get-NetAdapterStatistics | ForEach-Object {
17
+ $name = [string]$_.Name
18
+ if ($name -match 'vEthernet|Loopback|Bluetooth|WSL|Hyper-V|Virtual|Pseudo|isatap|Teredo') { return }
19
+ $rx += [int64]$_.ReceivedBytes
20
+ $tx += [int64]$_.SentBytes
21
+ }
22
+ Write-Output ("{0} {1} {2} {3}" -f $dr, $dw, $rx, $tx)
10
23
  }
11
24
  `.trim();
12
25
 
13
26
  /**
14
27
  * @param {string} line
15
- * @returns {{ rx: number, wx: number } | null}
28
+ * @returns {{ rx: number, wx: number, netRx: number, netTx: number } | null}
16
29
  */
17
30
  export function parseDiskCounterLine(line) {
18
- const m = String(line || "").trim().match(/^(\d+)\s+(\d+)$/);
31
+ const m = String(line || "")
32
+ .trim()
33
+ .match(/^(\d+)\s+(\d+)(?:\s+(\d+)\s+(\d+))?$/);
19
34
  if (!m) return null;
20
- return { rx: Number(m[1]), wx: Number(m[2]) };
35
+ return {
36
+ rx: Number(m[1]),
37
+ wx: Number(m[2]),
38
+ netRx: m[3] != null ? Number(m[3]) : 0,
39
+ netTx: m[4] != null ? Number(m[4]) : 0,
40
+ };
21
41
  }
22
42
 
23
43
  function encodedCommand(script) {
@@ -26,11 +46,12 @@ function encodedCommand(script) {
26
46
 
27
47
  /**
28
48
  * Long-lived PowerShell so WMI stays warm (~10ms/tick after first query).
49
+ * One round-trip: disk bytes + physical NIC byte counters.
29
50
  */
30
51
  export function createWindowsDiskReader() {
31
52
  let child = null;
32
53
  let buf = "";
33
- /** @type {{ resolve: (v: { rx: number, wx: number } | null) => void }[]} */
54
+ /** @type {{ resolve: (v: ReturnType<typeof parseDiskCounterLine>) => void }[]} */
34
55
  let pending = [];
35
56
  let closed = false;
36
57
 
@@ -43,7 +64,7 @@ export function createWindowsDiskReader() {
43
64
  if (child || closed) return;
44
65
  child = spawn(
45
66
  "powershell.exe",
46
- ["-NoProfile", "-NonInteractive", "-EncodedCommand", encodedCommand(PS_DISK)],
67
+ ["-NoProfile", "-NonInteractive", "-EncodedCommand", encodedCommand(PS_COUNTERS)],
47
68
  { windowsHide: true, stdio: ["pipe", "pipe", "ignore"] },
48
69
  );
49
70
  child.stdout.setEncoding("utf8");
@@ -67,7 +88,7 @@ export function createWindowsDiskReader() {
67
88
 
68
89
  return {
69
90
  /**
70
- * @returns {Promise<{ rx: number, wx: number } | null>}
91
+ * @returns {Promise<ReturnType<typeof parseDiskCounterLine>>}
71
92
  */
72
93
  read() {
73
94
  if (closed) return Promise.resolve(null);
package/src/format.js CHANGED
@@ -39,15 +39,19 @@ export function formatByteRate(bytesPerSec) {
39
39
  }
40
40
 
41
41
  /**
42
- * Network throughput in decimal Mbps (1 Mbps = 1e6 bit/s).
42
+ * Network throughput in decimal bit/s, auto-scaled (kbps / Mbps / Gbps).
43
+ * 1 kbps = 1e3 bit/s, 1 Mbps = 1e6 bit/s.
43
44
  * @param {number | null | undefined} bytesPerSec
44
45
  */
45
46
  export function formatBitRate(bytesPerSec) {
46
47
  if (bytesPerSec == null) return "—";
47
48
  const n = Number(bytesPerSec);
48
49
  if (!Number.isFinite(n) || n < 0) return "—";
49
- const mbps = (n * 8) / 1e6;
50
- return `${mbps.toFixed(1)} Mbps`;
50
+ const bits = n * 8;
51
+ if (bits >= 1e9) return `${(bits / 1e9).toFixed(2)} Gbps`;
52
+ if (bits >= 1e6) return `${(bits / 1e6).toFixed(1)} Mbps`;
53
+ if (bits >= 1e3) return `${(bits / 1e3).toFixed(1)} kbps`;
54
+ return `${bits.toFixed(0)} bps`;
51
55
  }
52
56
 
53
57
  /**
package/src/render.js CHANGED
@@ -140,7 +140,7 @@ export function renderFrame(snap, history, opts = {}) {
140
140
  if (snap.net) {
141
141
  const up = formatBitRate(snap.net.txBps);
142
142
  const down = formatBitRate(snap.net.rxBps);
143
- lines.push(`${color.yellow("NET")} ↑ ${up.padStart(10)} ↓ ${down.padStart(10)}`);
143
+ lines.push(`${color.yellow("NET")} ↑ ${up.padStart(12)} ↓ ${down.padStart(12)}`);
144
144
  lines.push(` ${color.yellow(sparkline(history.net, sparkW, { ascii }))}`);
145
145
  } else {
146
146
  lines.push(`${color.yellow("NET")} n/a`);
package/src/sample.js CHANGED
@@ -100,23 +100,22 @@ export function gpuBytes(n, vramMb) {
100
100
  */
101
101
  export function pickGpu(controllers) {
102
102
  if (!Array.isArray(controllers) || controllers.length === 0) return null;
103
- const usable = controllers.filter((c) => {
103
+ /** @type {GpuSample[]} */
104
+ const usable = [];
105
+ for (const c of controllers) {
104
106
  const util = num(c.utilizationGpu);
105
- const total = gpuBytes(num(c.memoryTotal), num(c.vram));
106
107
  const used = gpuBytes(num(c.memoryUsed), null);
107
- return util != null || (total != null && total > 0) || used != null;
108
- });
108
+ const total = gpuBytes(num(c.memoryTotal), null);
109
+ if (util == null && (used == null || total == null)) continue;
110
+ usable.push({
111
+ percent: util ?? 0,
112
+ used,
113
+ total,
114
+ });
115
+ }
109
116
  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
- };
117
+ usable.sort((a, b) => (b.total ?? 0) - (a.total ?? 0));
118
+ return usable[0];
120
119
  }
121
120
 
122
121
  /**
@@ -174,15 +173,16 @@ function mergePair(next, last, a, b) {
174
173
  /**
175
174
  * @param {os.CpuInfo[]} cpus
176
175
  */
177
- export function cpuIdleTotal(cpus) {
176
+ export function cpuIdleTotal(cpus, windows = process.platform === "win32") {
178
177
  let idle = 0;
179
178
  let total = 0;
180
179
  for (const c of cpus) {
181
180
  const t = c.times;
181
+ const irq = t.irq || 0;
182
+ const sys = windows ? Math.max(0, (t.sys || 0) - irq) : t.sys || 0;
182
183
  const i = t.idle || 0;
183
- const sum = (t.user || 0) + (t.nice || 0) + (t.sys || 0) + (t.irq || 0) + i;
184
184
  idle += i;
185
- total += sum;
185
+ total += (t.user || 0) + (t.nice || 0) + sys + irq + i;
186
186
  }
187
187
  return { idle, total };
188
188
  }
@@ -204,6 +204,16 @@ export function cpuPercentFromDelta(prev, curr) {
204
204
  /**
205
205
  * @param {Array<{ iface?: string, rx_bytes?: number, tx_bytes?: number, operstate?: string, internal?: boolean }>} stats
206
206
  */
207
+ const VIRTUAL_IFACE =
208
+ /^(lo|lo0)$|Loopback|vEthernet|Hyper-V|WSL|Bluetooth|Virtual|VPN|TAP|TUN|Tailscale|Pseudo|isatap|Teredo/i;
209
+
210
+ /**
211
+ * @param {string} name
212
+ */
213
+ export function isVirtualIface(name) {
214
+ return VIRTUAL_IFACE.test(String(name || ""));
215
+ }
216
+
207
217
  export function sumNetBytes(stats) {
208
218
  let rx = 0;
209
219
  let tx = 0;
@@ -211,12 +221,8 @@ export function sumNetBytes(stats) {
211
221
  if (!Array.isArray(stats)) return { rx: null, tx: null };
212
222
  for (const n of stats) {
213
223
  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
- }
224
+ if (n.internal || isVirtualIface(name)) continue;
225
+ if (n.operstate && n.operstate !== "up") continue;
220
226
  const r = num(n.rx_bytes);
221
227
  const t = num(n.tx_bytes);
222
228
  if (r == null && t == null) continue;
@@ -282,34 +288,28 @@ export function createSampler(si) {
282
288
  }
283
289
 
284
290
  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
291
  try {
299
292
  const load = await lib.currentLoad();
300
293
  const pct = num(load?.currentLoad);
301
294
  if (pct != null) return { percent: Math.min(100, Math.max(0, pct)) };
302
295
  } catch {
303
- // keep n/a
296
+ // fall through to os.cpus() idle-delta
304
297
  }
305
- return null;
298
+ const curr = cpuIdleTotal(os.cpus());
299
+ const pct = cpuPercentFromDelta(prevCpu, curr);
300
+ prevCpu = curr;
301
+ if (pct == null) return null;
302
+ return { percent: pct };
306
303
  }
307
304
 
308
305
  async function sampleRam(lib, ramTotal) {
309
306
  try {
310
307
  const mem = await lib.mem();
311
308
  const total = num(mem?.total) ?? ramTotal;
312
- const used = num(mem?.used) ?? num(mem?.active);
309
+ const available = num(mem?.available) ?? num(mem?.free);
310
+ let used = null;
311
+ if (total != null && available != null) used = Math.max(0, total - available);
312
+ else used = num(mem?.used) ?? num(mem?.active);
313
313
  if (used == null || total == null || total <= 0) return null;
314
314
  return {
315
315
  percent: Math.min(100, Math.max(0, (used / total) * 100)),
@@ -329,21 +329,20 @@ export function createSampler(si) {
329
329
  }
330
330
  }
331
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;
332
+ /**
333
+ * @param {number} ts
334
+ * @param {{ rx: number, wx: number } | null} win
335
+ */
336
+ async function sampleDisk(lib, ts, win) {
337
+ let rx = win ? num(win.rx) : null;
338
+ let wx = win ? num(win.wx) : null;
339
+ if (rx == null && wx == null) {
340
+ try {
341
+ const fs = await lib.fsStats();
342
+ rx = num(fs?.rx);
343
+ wx = num(fs?.wx);
344
+ } catch {
345
+ // Windows fsStats is always null
347
346
  }
348
347
  }
349
348
  if (rx == null && wx == null) return { readBps: null, writeBps: null };
@@ -353,17 +352,27 @@ export function createSampler(si) {
353
352
  return { readBps, writeBps };
354
353
  }
355
354
 
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 };
355
+ /**
356
+ * @param {number} ts
357
+ * @param {{ netRx?: number, netTx?: number } | null} win
358
+ */
359
+ async function sampleNet(lib, ts, win) {
360
+ let rx = win ? num(win.netRx) : null;
361
+ let tx = win ? num(win.netTx) : null;
362
+ if (rx == null && tx == null) {
363
+ try {
364
+ const stats = await lib.networkStats("*");
365
+ const summed = sumNetBytes(Array.isArray(stats) ? stats : stats ? [stats] : []);
366
+ rx = summed.rx;
367
+ tx = summed.tx;
368
+ } catch {
369
+ return { txBps: null, rxBps: null };
370
+ }
366
371
  }
372
+ const rxBps = rateFromCounters(prevNet?.rx, rx, prevTs, ts);
373
+ const txBps = rateFromCounters(prevNet?.tx, tx, prevTs, ts);
374
+ if (rx != null && tx != null) prevNet = { rx, tx };
375
+ return { txBps, rxBps };
367
376
  }
368
377
 
369
378
  async function sampleGpu(lib) {
@@ -383,12 +392,13 @@ export function createSampler(si) {
383
392
  const lib = await loadSi();
384
393
  if (!staticP) staticP = loadStatic();
385
394
  const meta = await staticP;
395
+ const win = winDisk ? await winDisk.read().catch(() => null) : null;
386
396
  const ts = Date.now();
387
397
  const [cpu, ram, disk, net, gpu] = await Promise.all([
388
398
  sampleCpu(lib).catch(() => last?.cpu ?? null),
389
399
  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 }),
400
+ sampleDisk(lib, ts, win).catch(() => last?.disk ?? { readBps: null, writeBps: null }),
401
+ sampleNet(lib, ts, win).catch(() => last?.net ?? { txBps: null, rxBps: null }),
392
402
  sampleGpu(lib).catch(() => last?.gpu ?? null),
393
403
  ]);
394
404
  prevTs = ts;