@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/format.js ADDED
@@ -0,0 +1,86 @@
1
+ const KIB = 1024;
2
+ const MIB = 1024 * 1024;
3
+ const GIB = 1024 * 1024 * 1024;
4
+
5
+ /**
6
+ * @param {number} bytes
7
+ */
8
+ export function formatBytes(bytes) {
9
+ const n = Number(bytes);
10
+ if (!Number.isFinite(n) || n < 0) return "n/a";
11
+ if (n >= GIB) return `${(n / GIB).toFixed(1)} GiB`;
12
+ return `${(n / MIB).toFixed(1)} MiB`;
13
+ }
14
+
15
+ /**
16
+ * @param {number} used
17
+ * @param {number} total
18
+ */
19
+ export function formatBytePair(used, total) {
20
+ const t = Number(total);
21
+ const u = Number(used);
22
+ if (!Number.isFinite(t) || t < 0 || !Number.isFinite(u) || u < 0) return "n/a";
23
+ const unit = t >= GIB ? "GiB" : "MiB";
24
+ const div = unit === "GiB" ? GIB : MIB;
25
+ return `${(u / div).toFixed(1)} / ${(t / div).toFixed(1)} ${unit}`;
26
+ }
27
+
28
+ /**
29
+ * Disk throughput. `null` → em dash (first tick).
30
+ * @param {number | null | undefined} bytesPerSec
31
+ */
32
+ export function formatByteRate(bytesPerSec) {
33
+ if (bytesPerSec == null) return "—";
34
+ const n = Number(bytesPerSec);
35
+ if (!Number.isFinite(n) || n < 0) return "—";
36
+ if (n >= GIB) return `${(n / GIB).toFixed(1)} GiB/s`;
37
+ if (n >= MIB) return `${(n / MIB).toFixed(1)} MiB/s`;
38
+ return `${(n / KIB).toFixed(1)} KiB/s`;
39
+ }
40
+
41
+ /**
42
+ * Network throughput in decimal Mbps (1 Mbps = 1e6 bit/s).
43
+ * @param {number | null | undefined} bytesPerSec
44
+ */
45
+ export function formatBitRate(bytesPerSec) {
46
+ if (bytesPerSec == null) return "—";
47
+ const n = Number(bytesPerSec);
48
+ if (!Number.isFinite(n) || n < 0) return "—";
49
+ const mbps = (n * 8) / 1e6;
50
+ return `${mbps.toFixed(1)} Mbps`;
51
+ }
52
+
53
+ /**
54
+ * @param {number} percent
55
+ */
56
+ export function formatPercent(percent) {
57
+ const n = Number(percent);
58
+ if (!Number.isFinite(n)) return "n/a";
59
+ const p = Math.min(100, Math.max(0, Math.round(n)));
60
+ return `${String(p).padStart(3, " ")}%`;
61
+ }
62
+
63
+ /**
64
+ * @param {number} seconds
65
+ */
66
+ export function formatInterval(seconds) {
67
+ const n = Number(seconds);
68
+ if (!Number.isFinite(n)) return "1.0s";
69
+ return `${n.toFixed(1)}s`;
70
+ }
71
+
72
+ /**
73
+ * @param {number} physical
74
+ * @param {number} logical
75
+ */
76
+ export function formatCores(physical, logical) {
77
+ const p = Number(physical);
78
+ const l = Number(logical);
79
+ const ps = Number.isFinite(p) && p > 0 ? String(Math.round(p)) : "?";
80
+ const ls = Number.isFinite(l) && l > 0 ? String(Math.round(l)) : "?";
81
+ return `${ps}P/${ls}L`;
82
+ }
83
+
84
+ export function formatCoresDetail(physical, logical) {
85
+ return formatCores(physical, logical).replace("/", " / ");
86
+ }
package/src/history.js ADDED
@@ -0,0 +1,58 @@
1
+ import { HISTORY_SIZE } from "./bar.js";
2
+
3
+ /**
4
+ * @typedef {object} History
5
+ * @property {number[]} cpu
6
+ * @property {number[]} ram
7
+ * @property {number[]} gpu
8
+ * @property {number[]} dsk
9
+ * @property {number[]} net
10
+ */
11
+
12
+ export function createHistory() {
13
+ return { cpu: [], ram: [], gpu: [], dsk: [], net: [] };
14
+ }
15
+
16
+ /**
17
+ * @param {number[]} buf
18
+ * @param {number | null | undefined} value
19
+ * @param {number} [size]
20
+ */
21
+ export function pushSample(buf, value, size = HISTORY_SIZE) {
22
+ if (value == null || Number.isNaN(Number(value))) return buf;
23
+ const n = Number(value);
24
+ if (!Number.isFinite(n)) return buf;
25
+ const next = buf.length >= size ? buf.slice(buf.length - size + 1) : buf.slice();
26
+ next.push(n);
27
+ return next;
28
+ }
29
+
30
+ /**
31
+ * @param {History} history
32
+ * @param {import("./sample.js").Snapshot} snap
33
+ * @returns {History}
34
+ */
35
+ export function appendHistory(history, snap) {
36
+ const cpu = snap.cpu?.percent;
37
+ const ram = snap.ram?.percent;
38
+ const gpu = snap.gpu?.percent;
39
+ const read = snap.disk?.readBps;
40
+ const write = snap.disk?.writeBps;
41
+ const tx = snap.net?.txBps;
42
+ const rx = snap.net?.rxBps;
43
+ let dsk;
44
+ if (read != null || write != null) {
45
+ dsk = (read ?? 0) + (write ?? 0);
46
+ }
47
+ let net;
48
+ if (tx != null || rx != null) {
49
+ net = (tx ?? 0) + (rx ?? 0);
50
+ }
51
+ return {
52
+ cpu: pushSample(history.cpu, cpu),
53
+ ram: pushSample(history.ram, ram),
54
+ gpu: pushSample(history.gpu, gpu),
55
+ dsk: pushSample(history.dsk, dsk),
56
+ net: pushSample(history.net, net),
57
+ };
58
+ }
package/src/index.js ADDED
@@ -0,0 +1,92 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { helpText, parseArgs } from "./args.js";
5
+ import { runDashboard } from "./dashboard.js";
6
+ import { CliError } from "./errors.js";
7
+ import { restoreTerminal } from "./tty.js";
8
+
9
+ const pkg = JSON.parse(
10
+ readFileSync(
11
+ path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json"),
12
+ "utf8",
13
+ ),
14
+ );
15
+
16
+ function defaultDeps() {
17
+ return {
18
+ env: process.env,
19
+ version: pkg.version,
20
+ log: (msg) => console.log(msg),
21
+ err: (msg) => console.error(msg),
22
+ write: (s) => process.stdout.write(s),
23
+ isTTY: () => Boolean(process.stdout.isTTY),
24
+ columns: () => process.stdout.columns || 80,
25
+ stdin: process.stdin,
26
+ process,
27
+ };
28
+ }
29
+
30
+ /**
31
+ * @param {string[]} argv
32
+ * @param {Partial<ReturnType<typeof defaultDeps>> & { dashboard?: Function }} [overrides]
33
+ * @returns {Promise<number>}
34
+ */
35
+ export async function main(argv, overrides = {}) {
36
+ const deps = { ...defaultDeps(), ...overrides };
37
+ let args;
38
+ try {
39
+ args = parseArgs(argv);
40
+ } catch (err) {
41
+ if (err instanceof CliError) {
42
+ deps.err(err.message);
43
+ return err.exitCode;
44
+ }
45
+ throw err;
46
+ }
47
+
48
+ if (args.help) {
49
+ deps.log(helpText());
50
+ return 0;
51
+ }
52
+ if (args.version) {
53
+ deps.log(deps.version);
54
+ return 0;
55
+ }
56
+
57
+ if (!deps.isTTY()) {
58
+ deps.err("yf-vitals needs a terminal.");
59
+ return 1;
60
+ }
61
+
62
+ const dash = deps.dashboard || runDashboard;
63
+ return dash(args, deps);
64
+ }
65
+
66
+ /**
67
+ * @param {string[]} argv
68
+ */
69
+ export async function run(argv) {
70
+ let entered = false;
71
+ const wrapWrite = (s) => process.stdout.write(s);
72
+ try {
73
+ const code = await main(argv, {
74
+ write: (s) => {
75
+ entered = entered || s.includes("\u001b[?1049h");
76
+ wrapWrite(s);
77
+ },
78
+ });
79
+ process.exit(typeof code === "number" ? code : 0);
80
+ } catch (err) {
81
+ if (entered) {
82
+ try {
83
+ restoreTerminal(wrapWrite, process.stdin);
84
+ } catch {
85
+ // last-ditch
86
+ }
87
+ }
88
+ const message = err instanceof CliError ? err.message : err?.message || String(err);
89
+ console.error(message);
90
+ process.exit(err instanceof CliError ? err.exitCode : 1);
91
+ }
92
+ }
package/src/rates.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Bytes/s (or any counter/s) from two cumulative counters.
3
+ * First sample returns null. Counter wrap or non-positive elapsed → null.
4
+ *
5
+ * @param {number | null | undefined} prev
6
+ * @param {number | null | undefined} current
7
+ * @param {number | null | undefined} prevTs
8
+ * @param {number | null | undefined} currentTs
9
+ * @returns {number | null}
10
+ */
11
+ export function rateFromCounters(prev, current, prevTs, currentTs) {
12
+ if (prev == null || current == null || prevTs == null || currentTs == null) {
13
+ return null;
14
+ }
15
+ const dtMs = Number(currentTs) - Number(prevTs);
16
+ if (!Number.isFinite(dtMs) || dtMs <= 0) return null;
17
+ const a = Number(prev);
18
+ const b = Number(current);
19
+ if (!Number.isFinite(a) || !Number.isFinite(b) || b < a) return null;
20
+ return (b - a) / (dtMs / 1000);
21
+ }
package/src/render.js ADDED
@@ -0,0 +1,153 @@
1
+ import { useAscii } from "./ascii.js";
2
+ import { BAR_WIDTH, bar, barCharset, sparkWidth, sparkline } from "./bar.js";
3
+ import { createColor, stripAnsi } from "./color.js";
4
+ import {
5
+ formatBitRate,
6
+ formatBytePair,
7
+ formatByteRate,
8
+ formatBytes,
9
+ formatCores,
10
+ formatCoresDetail,
11
+ formatInterval,
12
+ formatPercent,
13
+ } from "./format.js";
14
+
15
+ /**
16
+ * @param {string} line
17
+ * @param {number} columns
18
+ */
19
+ export function fitLine(line, columns) {
20
+ const cols = Number(columns);
21
+ if (!Number.isFinite(cols) || cols < 8) return line;
22
+ const plain = stripAnsi(line);
23
+ if (plain.length <= cols) return line;
24
+ if (plain === line) return line.slice(0, cols);
25
+ let out = "";
26
+ let visible = 0;
27
+ for (let i = 0; i < line.length; i += 1) {
28
+ if (line[i] === "\u001b") {
29
+ const end = line.indexOf("m", i);
30
+ if (end === -1) break;
31
+ out += line.slice(i, end + 1);
32
+ i = end;
33
+ continue;
34
+ }
35
+ if (visible >= cols) break;
36
+ out += line[i];
37
+ visible += 1;
38
+ }
39
+ return out;
40
+ }
41
+
42
+ /**
43
+ * @param {import("./sample.js").GpuSample | null | undefined} gpu
44
+ * @param {{ history: number[], ascii: boolean, color: ReturnType<typeof createColor>, sparkW: number }} opts
45
+ * @returns {string[] | null}
46
+ */
47
+ export function buildGpuRow(gpu, opts) {
48
+ if (gpu == null) return null;
49
+ const { ascii, color, sparkW, history } = opts;
50
+ const cs = barCharset(ascii);
51
+ const pct = gpu.percent;
52
+ const barStr =
53
+ pct == null ? null : bar(pct, BAR_WIDTH, cs);
54
+ const pctStr = pct == null ? "n/a" : formatPercent(pct);
55
+ const mem =
56
+ gpu.used != null && gpu.total != null ? formatBytePair(gpu.used, gpu.total) : "";
57
+ const label = color.green("GPU");
58
+ const head =
59
+ barStr == null
60
+ ? `${label} ${pctStr}${mem ? ` ${mem}` : ""}`
61
+ : `${label} [${color.green(barStr)}] ${pctStr}${mem ? ` ${mem}` : ""}`;
62
+ const spark = color.green(sparkline(history, sparkW, { ascii, max: 100 }));
63
+ return [head, ` ${spark}`];
64
+ }
65
+
66
+ /**
67
+ * @param {import("./sample.js").Snapshot} snap
68
+ * @param {import("./history.js").History} history
69
+ * @param {{ columns?: number, env?: NodeJS.ProcessEnv, isTTY?: boolean, intervalSec?: number }} [opts]
70
+ */
71
+ export function renderFrame(snap, history, opts = {}) {
72
+ const columns = opts.columns ?? 80;
73
+ const env = opts.env ?? {};
74
+ const ascii = useAscii(env);
75
+ const color = createColor({ isTTY: Boolean(opts.isTTY), env });
76
+ const sparkW = sparkWidth(columns);
77
+ const cs = barCharset(ascii);
78
+ const interval = formatInterval(opts.intervalSec ?? 1);
79
+ const cores = formatCores(snap.physical, snap.logical);
80
+ const ramTot = formatBytes(snap.ramTotal);
81
+ const header = [
82
+ "yf-vitals",
83
+ snap.hostname || "localhost",
84
+ snap.osName || "unknown",
85
+ cores,
86
+ ramTot,
87
+ interval,
88
+ ].join(" ");
89
+
90
+ /** @type {string[]} */
91
+ const lines = [header, ""];
92
+
93
+ if (snap.cpu) {
94
+ const b = color.cyan(bar(snap.cpu.percent, BAR_WIDTH, cs));
95
+ const detail = formatCoresDetail(snap.physical, snap.logical);
96
+ lines.push(
97
+ `${color.cyan("CPU")} [${b}] ${formatPercent(snap.cpu.percent)} ${detail}`,
98
+ );
99
+ lines.push(` ${color.cyan(sparkline(history.cpu, sparkW, { ascii, max: 100 }))}`);
100
+ } else {
101
+ lines.push(`${color.cyan("CPU")} n/a`);
102
+ lines.push(` ${color.cyan(sparkline(history.cpu, sparkW, { ascii, max: 100 }))}`);
103
+ }
104
+ lines.push("");
105
+
106
+ if (snap.ram) {
107
+ const b = color.magenta(bar(snap.ram.percent, BAR_WIDTH, cs));
108
+ lines.push(
109
+ `${color.magenta("RAM")} [${b}] ${formatPercent(snap.ram.percent)} ${formatBytePair(snap.ram.used, snap.ram.total)}`,
110
+ );
111
+ lines.push(` ${color.magenta(sparkline(history.ram, sparkW, { ascii, max: 100 }))}`);
112
+ } else {
113
+ lines.push(`${color.magenta("RAM")} n/a`);
114
+ lines.push(` ${color.magenta(sparkline(history.ram, sparkW, { ascii, max: 100 }))}`);
115
+ }
116
+ lines.push("");
117
+
118
+ const gpuLines = buildGpuRow(snap.gpu, {
119
+ history: history.gpu,
120
+ ascii,
121
+ color,
122
+ sparkW,
123
+ });
124
+ if (gpuLines) {
125
+ lines.push(...gpuLines);
126
+ lines.push("");
127
+ }
128
+
129
+ if (snap.disk) {
130
+ const r = formatByteRate(snap.disk.readBps);
131
+ const w = formatByteRate(snap.disk.writeBps);
132
+ lines.push(`${color.green("DSK")} R ${r.padStart(11)} W ${w.padStart(11)}`);
133
+ lines.push(` ${color.green(sparkline(history.dsk, sparkW, { ascii }))}`);
134
+ } else {
135
+ lines.push(`${color.green("DSK")} n/a`);
136
+ lines.push(` ${color.green(sparkline(history.dsk, sparkW, { ascii }))}`);
137
+ }
138
+ lines.push("");
139
+
140
+ if (snap.net) {
141
+ const up = formatBitRate(snap.net.txBps);
142
+ const down = formatBitRate(snap.net.rxBps);
143
+ lines.push(`${color.yellow("NET")} ↑ ${up.padStart(10)} ↓ ${down.padStart(10)}`);
144
+ lines.push(` ${color.yellow(sparkline(history.net, sparkW, { ascii }))}`);
145
+ } else {
146
+ lines.push(`${color.yellow("NET")} n/a`);
147
+ lines.push(` ${color.yellow(sparkline(history.net, sparkW, { ascii }))}`);
148
+ }
149
+ lines.push("");
150
+ lines.push("q quit");
151
+
152
+ return lines.map((ln) => fitLine(ln, columns)).join("\n");
153
+ }