@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 YourFam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # yf-vitals
2
+
3
+ Live CPU, RAM, disk, and network bars (plus GPU when the OS exposes it) in your terminal.
4
+
5
+ Works on any machine with **Node 20+**. No git repo. No API key. Press **q** to quit.
6
+
7
+ - **Source / README:** https://github.com/YourFam/yf-vitals
8
+ - **npm:** https://www.npmjs.com/package/@yourfam/yf-vitals
9
+ - **Unscoped alias:** https://www.npmjs.com/package/yf-vitals
10
+ - **Issues:** https://github.com/YourFam/yf-vitals/issues
11
+
12
+ ## Install
13
+
14
+ Node 20+.
15
+
16
+ ```bash
17
+ npx yf-vitals
18
+ npx yf-vitals --low-power
19
+ npx yf-vitals --interval 0.5
20
+
21
+ npm install -g @yourfam/yf-vitals
22
+ yf-vitals
23
+ ```
24
+
25
+ Friend one-liners: `npx @yourfam/yf-vitals` **and** `npx yf-vitals`.
26
+
27
+ `npx yf-vitals` is the unscoped alias of `@yourfam/yf-vitals` (same CLI, same version). Both are yours.
28
+
29
+ The command is `yf-vitals`. It does not need a git work tree; `cwd` can be anywhere.
30
+
31
+ ## What you see
32
+
33
+ One live dashboard. Refresh every **1.0s** (or `--interval`, or **2.0s** with `--low-power`).
34
+
35
+ | Row | Meaning |
36
+ |---|---|
37
+ | **CPU** | Overall utilization 0–100%, plus physical / logical core counts |
38
+ | **RAM** | Percent used, plus used / total (`GiB` / `MiB`) |
39
+ | **DSK** | Read and write **rates** (KiB/s, MiB/s, GiB/s) |
40
+ | **NET** | Send ↑ and receive ↓ in **Mbps** (1 Mbps = 1e6 bit/s) |
41
+ | **GPU** | Utilization and VRAM when telemetry exists; **the row is omitted** when it does not |
42
+
43
+ Header: `yf-vitals` · hostname · OS · `NP/NL` cores · RAM total · tick interval.
44
+
45
+ Each percent row has a 20-cell bar and a sparkline of the last 60 ticks. Disk and net sparklines scale to the **max in that buffer** (rates have no 100% ceiling). CPU / RAM / GPU sparklines are percent 0–100.
46
+
47
+ First disk/net tick may show `—` while counters settle.
48
+
49
+ `NO_COLOR` turns off ANSI. `YF_VITALS_ASCII=1` forces `#`/`-` bars and `_-=#` sparks (also used when `WT_SESSION` is missing and `TERM` looks dumb).
50
+
51
+ Pipes and CI print `yf-vitals needs a terminal.` and exit 1 (no redraw loop).
52
+
53
+ ## Flags
54
+
55
+ | Token | Default | Meaning |
56
+ |---|---|---|
57
+ | *(none)* | | Live dashboard, 1.0s tick |
58
+ | `--interval <seconds>` | `1` | Tick period, `0.25`–`10`. Also `--interval=1` |
59
+ | `--low-power` | off | Tick **2.0s** (wins over `--interval`) |
60
+ | `--help` / `-h` | | Usage. Exit 0 |
61
+ | `--version` / `-V` | | Package version. Exit 0 |
62
+
63
+ Quit: `q` / `Q` / Ctrl+C. The terminal is restored (cursor on, no leftover alt-screen).
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { run } from "../src/index.js";
4
+
5
+ run(process.argv);
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@yourfam/yf-vitals",
3
+ "version": "0.1.0",
4
+ "description": "Live terminal dashboard for CPU, RAM, disk, net, and optional GPU",
5
+ "type": "module",
6
+ "bin": {
7
+ "yf-vitals": "bin/yf-vitals.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "LICENSE",
13
+ "README.md"
14
+ ],
15
+ "engines": {
16
+ "node": ">=20"
17
+ },
18
+ "scripts": {
19
+ "test": "node --test test/*.test.js",
20
+ "yf-vitals": "node ./bin/yf-vitals.js"
21
+ },
22
+ "keywords": [
23
+ "cli",
24
+ "cpu",
25
+ "ram",
26
+ "dashboard",
27
+ "sysmon"
28
+ ],
29
+ "author": {
30
+ "name": "YourFam",
31
+ "email": "kamal@yourfam.co",
32
+ "url": "https://github.com/YourFam"
33
+ },
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/YourFam/yf-vitals.git"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/YourFam/yf-vitals/issues"
41
+ },
42
+ "homepage": "https://github.com/YourFam/yf-vitals#readme",
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "dependencies": {
47
+ "picocolors": "^1.1.1",
48
+ "systeminformation": "^5.27.11"
49
+ }
50
+ }
package/src/args.js ADDED
@@ -0,0 +1,104 @@
1
+ import { CliError } from "./errors.js";
2
+
3
+ const MIN_INTERVAL = 0.25;
4
+ const MAX_INTERVAL = 10;
5
+ const DEFAULT_INTERVAL = 1;
6
+ const LOW_POWER_INTERVAL = 2;
7
+
8
+ /**
9
+ * @typedef {object} ParsedArgs
10
+ * @property {boolean} help
11
+ * @property {boolean} version
12
+ * @property {number} interval
13
+ * @property {boolean} lowPower
14
+ */
15
+
16
+ /**
17
+ * @param {string} flag
18
+ * @param {string | undefined} value
19
+ */
20
+ function parseIntervalValue(flag, value) {
21
+ if (value == null || value === "" || value.startsWith("-")) {
22
+ throw new CliError(
23
+ `Flag ${flag} requires a number from ${MIN_INTERVAL} to ${MAX_INTERVAL}.\nSee yf-vitals --help`,
24
+ );
25
+ }
26
+ const n = Number(value);
27
+ if (!Number.isFinite(n) || n < MIN_INTERVAL || n > MAX_INTERVAL) {
28
+ throw new CliError(
29
+ `Flag ${flag} requires a number from ${MIN_INTERVAL} to ${MAX_INTERVAL}.\nSee yf-vitals --help`,
30
+ );
31
+ }
32
+ return n;
33
+ }
34
+
35
+ /**
36
+ * @param {string[]} argv process.argv
37
+ * @returns {ParsedArgs}
38
+ */
39
+ export function parseArgs(argv) {
40
+ const args = argv.slice(2);
41
+ let help = false;
42
+ let version = false;
43
+ let interval = DEFAULT_INTERVAL;
44
+ let intervalSet = false;
45
+ let lowPower = false;
46
+
47
+ for (let i = 0; i < args.length; i += 1) {
48
+ const a = args[i];
49
+ if (a === "--help" || a === "-h") {
50
+ help = true;
51
+ } else if (a === "--version" || a === "-V") {
52
+ version = true;
53
+ } else if (a === "--low-power") {
54
+ lowPower = true;
55
+ } else if (a === "--interval") {
56
+ interval = parseIntervalValue("--interval", args[i + 1]);
57
+ intervalSet = true;
58
+ i += 1;
59
+ } else if (a.startsWith("--interval=")) {
60
+ interval = parseIntervalValue("--interval", a.slice("--interval=".length));
61
+ intervalSet = true;
62
+ } else if (a.startsWith("-")) {
63
+ throw new CliError(`Unknown flag: ${a}\nSee yf-vitals --help`);
64
+ } else {
65
+ throw new CliError(`Unknown argument: ${a}\nSee yf-vitals --help`);
66
+ }
67
+ }
68
+
69
+ if (help || version) {
70
+ return {
71
+ help,
72
+ version,
73
+ interval: DEFAULT_INTERVAL,
74
+ lowPower: false,
75
+ };
76
+ }
77
+
78
+ if (lowPower) {
79
+ interval = LOW_POWER_INTERVAL;
80
+ } else if (!intervalSet) {
81
+ interval = DEFAULT_INTERVAL;
82
+ }
83
+
84
+ return { help: false, version: false, interval, lowPower };
85
+ }
86
+
87
+ export function helpText() {
88
+ return `yf-vitals — live CPU, RAM, disk, net (and GPU) bars in the terminal
89
+
90
+ Usage:
91
+ yf-vitals
92
+ yf-vitals --low-power
93
+ yf-vitals --interval 0.5
94
+
95
+ --interval <seconds> Tick period (${MIN_INTERVAL}–${MAX_INTERVAL}, default ${DEFAULT_INTERVAL})
96
+ --low-power Tick every ${LOW_POWER_INTERVAL}s (wins over --interval)
97
+ --help, -h This text
98
+ --version, -V Package version
99
+
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.
102
+ Disk / net sparklines scale to the max in the last 60 ticks (no 100% ceiling).
103
+ Needs a terminal (no pipes). Node 20+.`;
104
+ }
package/src/ascii.js ADDED
@@ -0,0 +1,12 @@
1
+ const DUMB_TERM = /^(dumb|unknown)?$/i;
2
+
3
+ /**
4
+ * ASCII bars when YF_VITALS_ASCII=1, or when WT_SESSION is missing and TERM looks dumb.
5
+ * @param {NodeJS.ProcessEnv} [env]
6
+ */
7
+ export function useAscii(env = process.env) {
8
+ if (env.YF_VITALS_ASCII === "1") return true;
9
+ const term = env.TERM ?? "";
10
+ const dumb = DUMB_TERM.test(term);
11
+ return !env.WT_SESSION && dumb;
12
+ }
package/src/bar.js ADDED
@@ -0,0 +1,83 @@
1
+ export const BAR_WIDTH = 20;
2
+ export const HISTORY_SIZE = 60;
3
+ export const SPARK_MIN = 16;
4
+ export const SPARK_MAX = 60;
5
+ export const SPARK_GUTTER = 36;
6
+
7
+ export const UNI_BAR_FILLED = "█";
8
+ export const UNI_BAR_EMPTY = "░";
9
+ export const ASC_BAR_FILLED = "#";
10
+ export const ASC_BAR_EMPTY = "-";
11
+
12
+ export const UNI_SPARK = "▁▂▃▄▅▆▇█";
13
+ export const ASC_SPARK = "_-=#";
14
+
15
+ /**
16
+ * @param {number} percent
17
+ * @param {number} width
18
+ * @param {{ filled?: string, empty?: string }} [opts]
19
+ */
20
+ export function bar(percent, width, opts = {}) {
21
+ const filled = opts.filled ?? UNI_BAR_FILLED;
22
+ const empty = opts.empty ?? UNI_BAR_EMPTY;
23
+ const w = Math.max(0, Math.floor(width));
24
+ const n = Number(percent);
25
+ const p = Number.isFinite(n) ? Math.min(100, Math.max(0, n)) : 0;
26
+ const fillCount = w === 0 ? 0 : Math.round((p / 100) * w);
27
+ return filled.repeat(fillCount) + empty.repeat(w - fillCount);
28
+ }
29
+
30
+ /**
31
+ * @param {boolean} ascii
32
+ */
33
+ export function barCharset(ascii) {
34
+ return ascii
35
+ ? { filled: ASC_BAR_FILLED, empty: ASC_BAR_EMPTY }
36
+ : { filled: UNI_BAR_FILLED, empty: UNI_BAR_EMPTY };
37
+ }
38
+
39
+ /**
40
+ * @param {number} columns
41
+ */
42
+ export function sparkWidth(columns) {
43
+ const cols = Number(columns);
44
+ const available = Number.isFinite(cols) && cols > 0 ? cols - SPARK_GUTTER : SPARK_MAX;
45
+ return Math.max(SPARK_MIN, Math.min(SPARK_MAX, available));
46
+ }
47
+
48
+ /**
49
+ * @param {number[]} values
50
+ * @param {number} width
51
+ * @param {{ ascii?: boolean, max?: number }} [opts]
52
+ */
53
+ export function sparkline(values, width, opts = {}) {
54
+ const w = Math.max(0, Math.floor(width));
55
+ if (w === 0) return "";
56
+ const charset = opts.ascii ? ASC_SPARK : UNI_SPARK;
57
+ const list = Array.isArray(values) ? values : [];
58
+ if (list.length === 0) {
59
+ return " ".repeat(w);
60
+ }
61
+ const slice = list.slice(-w);
62
+ const pad = w - slice.length;
63
+ const explicitMax = opts.max;
64
+ const dataMax = slice.reduce((m, v) => (v > m ? v : m), 0);
65
+ const max = explicitMax != null ? explicitMax : dataMax;
66
+ const chars = slice.map((v) => sparkChar(v, max, charset));
67
+ return " ".repeat(pad) + chars.join("");
68
+ }
69
+
70
+ /**
71
+ * @param {number} value
72
+ * @param {number} max
73
+ * @param {string} charset
74
+ */
75
+ function sparkChar(value, max, charset) {
76
+ const n = Number(value);
77
+ if (!Number.isFinite(n) || n <= 0 || !Number.isFinite(max) || max <= 0) {
78
+ return charset[0];
79
+ }
80
+ const ratio = Math.min(1, n / max);
81
+ const idx = Math.round(ratio * (charset.length - 1));
82
+ return charset[idx];
83
+ }
package/src/color.js ADDED
@@ -0,0 +1,24 @@
1
+ import picocolors from "picocolors";
2
+
3
+ /**
4
+ * @param {{ isTTY: boolean, env: NodeJS.ProcessEnv }} opts
5
+ */
6
+ export function createColor(opts) {
7
+ const enabled = Boolean(opts.isTTY) && opts.env.NO_COLOR === undefined;
8
+ const pc = picocolors.createColors(enabled);
9
+ return {
10
+ enabled,
11
+ cyan: (value) => pc.cyan(String(value)),
12
+ magenta: (value) => pc.magenta(String(value)),
13
+ green: (value) => pc.green(String(value)),
14
+ yellow: (value) => pc.yellow(String(value)),
15
+ dim: (value) => pc.dim(String(value)),
16
+ };
17
+ }
18
+
19
+ /**
20
+ * @param {string} value
21
+ */
22
+ export function stripAnsi(value) {
23
+ return String(value).replace(/\u001b\[[0-9;]*m/g, "");
24
+ }
@@ -0,0 +1,125 @@
1
+ import { appendHistory, createHistory } from "./history.js";
2
+ import { renderFrame } from "./render.js";
3
+ import { createSampler, mergeLastGood } from "./sample.js";
4
+ import { CLEAR_HOME, enterTerminal, restoreTerminal } from "./tty.js";
5
+
6
+ const CTRL_C = 0x03;
7
+
8
+ /**
9
+ * @param {Buffer | string} chunk
10
+ */
11
+ function isQuitKey(chunk) {
12
+ if (chunk == null) return false;
13
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8");
14
+ if (buf.length === 0) return false;
15
+ if (buf[0] === CTRL_C) return true;
16
+ const s = buf.toString("utf8");
17
+ return s.includes("q") || s.includes("Q");
18
+ }
19
+
20
+ /**
21
+ * @param {number} ms
22
+ * @param {() => boolean} shouldStop
23
+ */
24
+ export function sleep(ms, shouldStop) {
25
+ const n = Math.max(0, Number(ms) || 0);
26
+ return new Promise((resolve) => {
27
+ if (n === 0 || shouldStop()) {
28
+ resolve();
29
+ return;
30
+ }
31
+ let settled = false;
32
+ const done = () => {
33
+ if (settled) return;
34
+ settled = true;
35
+ clearTimeout(t);
36
+ clearInterval(iv);
37
+ resolve();
38
+ };
39
+ const t = setTimeout(done, n);
40
+ const iv = setInterval(() => {
41
+ if (shouldStop()) done();
42
+ }, 50);
43
+ });
44
+ }
45
+
46
+ /**
47
+ * @param {import("./args.js").ParsedArgs} args
48
+ * @param {object} deps
49
+ */
50
+ export async function runDashboard(args, deps) {
51
+ const write = deps.write;
52
+ const stdin = deps.stdin;
53
+ let quit = false;
54
+ let restored = false;
55
+ const shouldStop = () => quit || Boolean(deps.shouldStop?.());
56
+
57
+ const restore = () => {
58
+ if (restored) return;
59
+ restored = true;
60
+ restoreTerminal(write, stdin);
61
+ };
62
+
63
+ enterTerminal(write, stdin);
64
+
65
+ const onData = (chunk) => {
66
+ if (isQuitKey(chunk)) quit = true;
67
+ };
68
+ const onSig = () => {
69
+ quit = true;
70
+ };
71
+
72
+ const proc = deps.process ?? process;
73
+ if (stdin && typeof stdin.on === "function") stdin.on("data", onData);
74
+ if (proc && typeof proc.on === "function") {
75
+ proc.on("SIGINT", onSig);
76
+ proc.on("SIGTERM", onSig);
77
+ }
78
+
79
+ /** @type {{ sample: Function, close?: Function } | null} */
80
+ let sampler = null;
81
+ try {
82
+ sampler = deps.sampler || createSampler();
83
+ let history = createHistory();
84
+ /** @type {import("./sample.js").Snapshot | null} */
85
+ let last = null;
86
+
87
+ while (!shouldStop()) {
88
+ let snap;
89
+ try {
90
+ snap = await sampler.sample();
91
+ } catch {
92
+ snap = last;
93
+ }
94
+ if (!snap) {
95
+ await (deps.sleep || sleep)(args.interval * 1000, shouldStop);
96
+ continue;
97
+ }
98
+ snap = mergeLastGood(last, snap);
99
+ last = snap;
100
+ history = appendHistory(history, snap);
101
+ const frame = renderFrame(snap, history, {
102
+ columns: deps.columns(),
103
+ env: deps.env,
104
+ isTTY: true,
105
+ intervalSec: args.interval,
106
+ });
107
+ write(CLEAR_HOME + frame);
108
+ if (shouldStop()) break;
109
+ await (deps.sleep || sleep)(args.interval * 1000, shouldStop);
110
+ }
111
+ return 0;
112
+ } finally {
113
+ try {
114
+ sampler?.close?.();
115
+ } catch {
116
+ // ignore
117
+ }
118
+ if (stdin && typeof stdin.off === "function") stdin.off("data", onData);
119
+ if (proc && typeof proc.off === "function") {
120
+ proc.off("SIGINT", onSig);
121
+ proc.off("SIGTERM", onSig);
122
+ }
123
+ restore();
124
+ }
125
+ }
package/src/disk.js ADDED
@@ -0,0 +1,110 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ const PS_DISK = `
4
+ $ErrorActionPreference = 'SilentlyContinue'
5
+ while (($line = [Console]::In.ReadLine()) -ne $null) {
6
+ if ($line -eq 'q') { break }
7
+ $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" }
10
+ }
11
+ `.trim();
12
+
13
+ /**
14
+ * @param {string} line
15
+ * @returns {{ rx: number, wx: number } | null}
16
+ */
17
+ export function parseDiskCounterLine(line) {
18
+ const m = String(line || "").trim().match(/^(\d+)\s+(\d+)$/);
19
+ if (!m) return null;
20
+ return { rx: Number(m[1]), wx: Number(m[2]) };
21
+ }
22
+
23
+ function encodedCommand(script) {
24
+ return Buffer.from(script, "utf16le").toString("base64");
25
+ }
26
+
27
+ /**
28
+ * Long-lived PowerShell so WMI stays warm (~10ms/tick after first query).
29
+ */
30
+ export function createWindowsDiskReader() {
31
+ let child = null;
32
+ let buf = "";
33
+ /** @type {{ resolve: (v: { rx: number, wx: number } | null) => void }[]} */
34
+ let pending = [];
35
+ let closed = false;
36
+
37
+ function flushLine(line) {
38
+ const job = pending.shift();
39
+ if (job) job.resolve(parseDiskCounterLine(line));
40
+ }
41
+
42
+ function ensure() {
43
+ if (child || closed) return;
44
+ child = spawn(
45
+ "powershell.exe",
46
+ ["-NoProfile", "-NonInteractive", "-EncodedCommand", encodedCommand(PS_DISK)],
47
+ { windowsHide: true, stdio: ["pipe", "pipe", "ignore"] },
48
+ );
49
+ child.stdout.setEncoding("utf8");
50
+ child.stdout.on("data", (chunk) => {
51
+ buf += chunk;
52
+ let idx;
53
+ while ((idx = buf.indexOf("\n")) >= 0) {
54
+ const line = buf.slice(0, idx);
55
+ buf = buf.slice(idx + 1);
56
+ flushLine(line);
57
+ }
58
+ });
59
+ child.on("exit", () => {
60
+ child = null;
61
+ buf = "";
62
+ while (pending.length) {
63
+ pending.shift()?.resolve(null);
64
+ }
65
+ });
66
+ }
67
+
68
+ return {
69
+ /**
70
+ * @returns {Promise<{ rx: number, wx: number } | null>}
71
+ */
72
+ read() {
73
+ if (closed) return Promise.resolve(null);
74
+ ensure();
75
+ if (!child || !child.stdin.writable) return Promise.resolve(null);
76
+ return new Promise((resolve) => {
77
+ const t = setTimeout(() => {
78
+ const i = pending.findIndex((p) => p.resolve === done);
79
+ if (i >= 0) pending.splice(i, 1);
80
+ resolve(null);
81
+ }, 2500);
82
+ const done = (v) => {
83
+ clearTimeout(t);
84
+ resolve(v);
85
+ };
86
+ pending.push({ resolve: done });
87
+ try {
88
+ child.stdin.write("g\n");
89
+ } catch {
90
+ clearTimeout(t);
91
+ resolve(null);
92
+ }
93
+ });
94
+ },
95
+ close() {
96
+ closed = true;
97
+ try {
98
+ child?.stdin.write("q\n");
99
+ } catch {
100
+ // ignore
101
+ }
102
+ try {
103
+ child?.kill();
104
+ } catch {
105
+ // ignore
106
+ }
107
+ child = null;
108
+ },
109
+ };
110
+ }
package/src/errors.js ADDED
@@ -0,0 +1,11 @@
1
+ export class CliError extends Error {
2
+ /**
3
+ * @param {string} message
4
+ * @param {number} [exitCode]
5
+ */
6
+ constructor(message, exitCode = 1) {
7
+ super(message);
8
+ this.name = "CliError";
9
+ this.exitCode = exitCode;
10
+ }
11
+ }