pyre-cli 5.1.0 → 6.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/README.md +1 -1
- package/dist/index.js +922 -367
- package/package.json +1 -1
- package/src/completions.ts +118 -0
- package/src/diffCmd.ts +36 -0
- package/src/doctor.ts +123 -0
- package/src/formatters/index.ts +1 -1
- package/src/formatters/output.ts +14 -13
- package/src/formatters/render.ts +305 -114
- package/src/formatters/themes.ts +47 -12
- package/src/formatters/types.ts +6 -1
- package/src/historyCmd.ts +71 -0
- package/src/index.ts +129 -24
- package/src/live/input.ts +14 -5
- package/src/live/render.ts +126 -90
- package/src/live/state.ts +3 -0
- package/src/live/types.ts +6 -1
- package/src/monitors/collectors.ts +160 -36
- package/src/monitors/smc.ts +60 -12
- package/src/monitors/types.ts +2 -0
- package/src/p2p/server.test.ts +2 -2
- package/src/sparkline.ts +82 -13
- package/src/state/config.ts +6 -1
- package/Formula/pyre.rb +0 -19
package/package.json
CHANGED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export function generateZshCompletions(): string {
|
|
2
|
+
return `#compdef pyre
|
|
3
|
+
|
|
4
|
+
_pyre() {
|
|
5
|
+
local -a commands
|
|
6
|
+
commands=(
|
|
7
|
+
'live:Start interactive live monitoring dashboard'
|
|
8
|
+
'info:Display static system overview'
|
|
9
|
+
'doctor:Run system permissions and diagnostic checks'
|
|
10
|
+
'web:Launch web dashboard server'
|
|
11
|
+
'config:Show or reset configuration options'
|
|
12
|
+
'p2p:P2P streaming server or client'
|
|
13
|
+
'ssh:Stream metrics over SSH'
|
|
14
|
+
'bench:Benchmark command while logging system stats'
|
|
15
|
+
'completions:Generate shell completion scripts'
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_arguments -s \\
|
|
19
|
+
'(-j --json)'{-j,--json}'[Output snapshot as JSON]' \\
|
|
20
|
+
'--html[Output snapshot as HTML]' \\
|
|
21
|
+
'--md[Output snapshot as Markdown]' \\
|
|
22
|
+
'(-c --csv)'{-c,--csv}'[Output snapshot as CSV]' \\
|
|
23
|
+
'(-t --tsv)'{-t,--tsv}'[Output snapshot as TSV]' \\
|
|
24
|
+
'--detailed[Include detailed system info and sensor readings]' \\
|
|
25
|
+
'--theme[Default theme]:theme:(default dracula cyberpunk monochrome nord gruvbox)' \\
|
|
26
|
+
'--interval[Refresh interval in seconds]:seconds:' \\
|
|
27
|
+
'--once[Show a single static snapshot]' \\
|
|
28
|
+
'--out[Write snapshot output to file]:file:_files' \\
|
|
29
|
+
'--export-dir[Directory for snapshot exports and logs]:directory:_files -/' \\
|
|
30
|
+
'--log[Start continuous CSV logging]' \\
|
|
31
|
+
'--tree[Show process tree view]' \\
|
|
32
|
+
'--sort[Sort processes by]:key:(cpu mem pid user command state threads runtime)' \\
|
|
33
|
+
'--packets[Include packet monitor panel]' \\
|
|
34
|
+
'--limit[Max processes to include]:count:' \\
|
|
35
|
+
'--alert-cpu[CPU alert threshold pct]:pct:' \\
|
|
36
|
+
'--alert-temp[CPU alert threshold temp C]:temp:' \\
|
|
37
|
+
'--temp-unit[Temperature display unit]:unit:(c f)' \\
|
|
38
|
+
'*::command:->command'
|
|
39
|
+
|
|
40
|
+
case $state in
|
|
41
|
+
command)
|
|
42
|
+
_describe -t commands 'pyre command' commands
|
|
43
|
+
;;
|
|
44
|
+
esac
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_pyre "$@"
|
|
48
|
+
`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function generateBashCompletions(): string {
|
|
52
|
+
return `# bash completion for pyre
|
|
53
|
+
|
|
54
|
+
_pyre_completions() {
|
|
55
|
+
local cur prev opts commands
|
|
56
|
+
COMPREPLY=()
|
|
57
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
58
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
59
|
+
commands="live info doctor web config p2p ssh bench completions"
|
|
60
|
+
opts="--json --html --md --csv --tsv --detailed --theme --interval --once --out --export-dir --log --tree --sort --packets --limit --alert-cpu --alert-temp --temp-unit"
|
|
61
|
+
|
|
62
|
+
case "\${prev}" in
|
|
63
|
+
--theme)
|
|
64
|
+
COMPREPLY=( $(compgen -W "default dracula cyberpunk monochrome nord gruvbox" -- \${cur}) )
|
|
65
|
+
return 0
|
|
66
|
+
;;
|
|
67
|
+
--sort)
|
|
68
|
+
COMPREPLY=( $(compgen -W "cpu mem pid user command state threads runtime" -- \${cur}) )
|
|
69
|
+
return 0
|
|
70
|
+
;;
|
|
71
|
+
--temp-unit)
|
|
72
|
+
COMPREPLY=( $(compgen -W "c f" -- \${cur}) )
|
|
73
|
+
return 0
|
|
74
|
+
;;
|
|
75
|
+
config)
|
|
76
|
+
COMPREPLY=( $(compgen -W "show reset" -- \${cur}) )
|
|
77
|
+
return 0
|
|
78
|
+
;;
|
|
79
|
+
p2p)
|
|
80
|
+
COMPREPLY=( $(compgen -W "server connect" -- \${cur}) )
|
|
81
|
+
return 0
|
|
82
|
+
;;
|
|
83
|
+
esac
|
|
84
|
+
|
|
85
|
+
if [[ \${cur} == -* ]] ; then
|
|
86
|
+
COMPREPLY=( $(compgen -W "\${opts}" -- \${cur}) )
|
|
87
|
+
return 0
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
COMPREPLY=( $(compgen -W "\${commands}" -- \${cur}) )
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
complete -F _pyre_completions pyre
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function generateFishCompletions(): string {
|
|
98
|
+
return `# fish completion for pyre
|
|
99
|
+
|
|
100
|
+
complete -c pyre -n "__fish_use_subcommand" -a live -d "Start interactive live monitoring dashboard"
|
|
101
|
+
complete -c pyre -n "__fish_use_subcommand" -a info -d "Display static system overview"
|
|
102
|
+
complete -c pyre -n "__fish_use_subcommand" -a doctor -d "Run system diagnostics"
|
|
103
|
+
complete -c pyre -n "__fish_use_subcommand" -a web -d "Launch web dashboard server"
|
|
104
|
+
complete -c pyre -n "__fish_use_subcommand" -a config -d "Show or reset configuration options"
|
|
105
|
+
complete -c pyre -n "__fish_use_subcommand" -a p2p -d "P2P streaming mode"
|
|
106
|
+
complete -c pyre -n "__fish_use_subcommand" -a completions -d "Generate shell completions"
|
|
107
|
+
|
|
108
|
+
complete -c pyre -l json -s j -d "Output snapshot as JSON"
|
|
109
|
+
complete -c pyre -l html -d "Output snapshot as HTML"
|
|
110
|
+
complete -c pyre -l md -d "Output snapshot as Markdown"
|
|
111
|
+
complete -c pyre -l csv -s c -d "Output snapshot as CSV"
|
|
112
|
+
complete -c pyre -l tsv -s t -d "Output snapshot as TSV"
|
|
113
|
+
complete -c pyre -l detailed -d "Include detailed system info and sensor readings"
|
|
114
|
+
complete -c pyre -l theme -x -a "default dracula cyberpunk monochrome nord gruvbox" -d "Set color theme"
|
|
115
|
+
complete -c pyre -l sort -x -a "cpu mem pid user command state threads runtime" -d "Sort processes by key"
|
|
116
|
+
complete -c pyre -l temp-unit -x -a "c f" -d "Temperature unit"
|
|
117
|
+
`;
|
|
118
|
+
}
|
package/src/diffCmd.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import type { StatsData } from './monitors/types.js';
|
|
4
|
+
|
|
5
|
+
export function runDiffCommand(file1: string, file2: string): void {
|
|
6
|
+
console.log(chalk.bold(`\n pyre diff — Snapshot Comparison\n`));
|
|
7
|
+
console.log(chalk.dim(` Snapshot A: ${file1}`));
|
|
8
|
+
console.log(chalk.dim(` Snapshot B: ${file2}\n`));
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const dataA: StatsData = JSON.parse(fs.readFileSync(file1, 'utf-8'));
|
|
12
|
+
const dataB: StatsData = JSON.parse(fs.readFileSync(file2, 'utf-8'));
|
|
13
|
+
|
|
14
|
+
const diffNum = (label: string, valA: number, valB: number, unit = '') => {
|
|
15
|
+
const delta = valB - valA;
|
|
16
|
+
const sign = delta > 0 ? '+' : '';
|
|
17
|
+
const color = delta > 0 ? chalk.red : delta < 0 ? chalk.green : chalk.dim;
|
|
18
|
+
console.log(` ${label.padEnd(20)} ${valA.toFixed(1)}${unit} ➔ ${valB.toFixed(1)}${unit} (${color(sign + delta.toFixed(1) + unit)})`);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
diffNum('CPU Usage', dataA.cpu.usage, dataB.cpu.usage, '%');
|
|
22
|
+
diffNum('Memory Usage', dataA.memory.usagePercent, dataB.memory.usagePercent, '%');
|
|
23
|
+
if (dataA.cpu.temperature && dataB.cpu.temperature) {
|
|
24
|
+
diffNum('CPU Temp', dataA.cpu.temperature, dataB.cpu.temperature, '°C');
|
|
25
|
+
}
|
|
26
|
+
if (dataA.gpu && dataB.gpu) {
|
|
27
|
+
diffNum('GPU Util', dataA.gpu.utilization, dataB.gpu.utilization, '%');
|
|
28
|
+
}
|
|
29
|
+
diffNum('Network RX Packets', dataA.network.rxPackets, dataB.network.rxPackets);
|
|
30
|
+
diffNum('Network TX Packets', dataA.network.txPackets, dataB.network.txPackets);
|
|
31
|
+
|
|
32
|
+
console.log();
|
|
33
|
+
} catch (err: any) {
|
|
34
|
+
console.log(chalk.red(`Failed to compare snapshots: ${err.message}`));
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/doctor.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { run } from './monitors/run.js';
|
|
3
|
+
import { readConfig, CONFIG_FILE } from './state/config.js';
|
|
4
|
+
|
|
5
|
+
export interface DoctorCheck {
|
|
6
|
+
name: string;
|
|
7
|
+
status: 'ok' | 'warn' | 'error';
|
|
8
|
+
message: string;
|
|
9
|
+
details?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function runDoctor(): Promise<DoctorCheck[]> {
|
|
13
|
+
const checks: DoctorCheck[] = [];
|
|
14
|
+
|
|
15
|
+
// 1. Sudo / Powermetrics Access
|
|
16
|
+
try {
|
|
17
|
+
const pmResult = await run('sudo -n powermetrics --samplers cpu_power -n 1 2>&1', '');
|
|
18
|
+
if (pmResult.includes('Password:') || pmResult.includes('Permission denied') || pmResult.includes('sudo: a password is required')) {
|
|
19
|
+
checks.push({
|
|
20
|
+
name: 'Sudo / Powermetrics',
|
|
21
|
+
status: 'warn',
|
|
22
|
+
message: 'Passwordless sudo for powermetrics is not configured.',
|
|
23
|
+
details: 'Powermetrics allows high-precision GPU & CPU power sampling. Run with sudo or configure passwordless sudo in /etc/sudoers.',
|
|
24
|
+
});
|
|
25
|
+
} else {
|
|
26
|
+
checks.push({
|
|
27
|
+
name: 'Sudo / Powermetrics',
|
|
28
|
+
status: 'ok',
|
|
29
|
+
message: 'Powermetrics access available.',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
checks.push({
|
|
34
|
+
name: 'Sudo / Powermetrics',
|
|
35
|
+
status: 'warn',
|
|
36
|
+
message: 'Powermetrics check failed.',
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 2. TCC / System Permissions
|
|
41
|
+
try {
|
|
42
|
+
const psCheck = await run('ps -ax -o pid,user,command', '');
|
|
43
|
+
if (psCheck.length > 0) {
|
|
44
|
+
checks.push({
|
|
45
|
+
name: 'TCC / Process Visibility',
|
|
46
|
+
status: 'ok',
|
|
47
|
+
message: 'Process listing access available.',
|
|
48
|
+
});
|
|
49
|
+
} else {
|
|
50
|
+
checks.push({
|
|
51
|
+
name: 'TCC / Process Visibility',
|
|
52
|
+
status: 'warn',
|
|
53
|
+
message: 'Restricted process visibility.',
|
|
54
|
+
details: 'Grant Full Disk Access or Terminal permissions in System Settings -> Privacy & Security.',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
checks.push({
|
|
59
|
+
name: 'TCC / Process Visibility',
|
|
60
|
+
status: 'error',
|
|
61
|
+
message: 'Unable to list system processes.',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 3. Network Reachability for P2P
|
|
66
|
+
try {
|
|
67
|
+
const netCheck = await run('netstat -rn', '');
|
|
68
|
+
if (netCheck.includes('default') || netCheck.includes('gateway')) {
|
|
69
|
+
checks.push({
|
|
70
|
+
name: 'Network Reachability',
|
|
71
|
+
status: 'ok',
|
|
72
|
+
message: 'Network interface active with default gateway.',
|
|
73
|
+
});
|
|
74
|
+
} else {
|
|
75
|
+
checks.push({
|
|
76
|
+
name: 'Network Reachability',
|
|
77
|
+
status: 'warn',
|
|
78
|
+
message: 'No active network route detected for P2P mode.',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
checks.push({
|
|
83
|
+
name: 'Network Reachability',
|
|
84
|
+
status: 'warn',
|
|
85
|
+
message: 'Could not verify network routes.',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 4. Config file integrity
|
|
90
|
+
try {
|
|
91
|
+
const cfg = readConfig();
|
|
92
|
+
checks.push({
|
|
93
|
+
name: 'Configuration File',
|
|
94
|
+
status: 'ok',
|
|
95
|
+
message: `Config file valid (${CONFIG_FILE}).`,
|
|
96
|
+
});
|
|
97
|
+
} catch (err: any) {
|
|
98
|
+
checks.push({
|
|
99
|
+
name: 'Configuration File',
|
|
100
|
+
status: 'error',
|
|
101
|
+
message: 'Failed to parse config file.',
|
|
102
|
+
details: err.message,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return checks;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function printDoctorReport(checks: DoctorCheck[]): void {
|
|
110
|
+
console.log(chalk.bold('\n pyre doctor — System Diagnostics\n'));
|
|
111
|
+
|
|
112
|
+
for (const check of checks) {
|
|
113
|
+
let icon = chalk.green('✔');
|
|
114
|
+
if (check.status === 'warn') icon = chalk.yellow('⚠');
|
|
115
|
+
if (check.status === 'error') icon = chalk.red('✖');
|
|
116
|
+
|
|
117
|
+
console.log(` ${icon} ${chalk.bold(check.name.padEnd(26))} ${check.message}`);
|
|
118
|
+
if (check.details) {
|
|
119
|
+
console.log(chalk.dim(` ↳ ${check.details}`));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
console.log();
|
|
123
|
+
}
|
package/src/formatters/index.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - `render.ts` — dashboard layout, cards, tables, and the main `formatTable`
|
|
12
12
|
* - `output.ts` — JSON/CSV/TSV export and sparkline graph rendering
|
|
13
13
|
*/
|
|
14
|
-
export { formatTable, clampWidth, gridColumns, thermalColor, capacityColor, getTabHitboxes, TAB_BAR_ROW, TAB_DEFS, panel, formatBytes } from './render.js';
|
|
14
|
+
export { formatTable, clampWidth, gridColumns, thermalColor, capacityColor, getTabHitboxes, TAB_BAR_ROW, TAB_DEFS, panel, formatBytes, padVisible, visLen, fitVisible } from './render.js';
|
|
15
15
|
export { formatJson, formatCsv, formatTsv, formatGraphs, formatHtml, formatMarkdown } from './output.js';
|
|
16
16
|
export type { ThemeName, ThemeColors, VisibleItems, TableOptions, AnomalyAlert } from './types.js';
|
|
17
17
|
export { THEMES } from './themes.js';
|
package/src/formatters/output.ts
CHANGED
|
@@ -10,8 +10,7 @@ import { sparkline } from '../sparkline.js';
|
|
|
10
10
|
import type { History } from '../history.js';
|
|
11
11
|
import type { StatsData } from './types.js';
|
|
12
12
|
import { THEMES, type ThemeName } from './themes.js';
|
|
13
|
-
import { formatBytes, celsiusToFahrenheit, formatTemp } from './render.js';
|
|
14
|
-
import { panel } from './render.js';
|
|
13
|
+
import { formatBytes, celsiusToFahrenheit, formatTemp, panel, fitVisible } from './render.js';
|
|
15
14
|
|
|
16
15
|
export function formatJson(data: StatsData): string {
|
|
17
16
|
return JSON.stringify(data, null, 2);
|
|
@@ -434,24 +433,24 @@ export function formatGraphs(history: History, width = 80, themeName: ThemeName
|
|
|
434
433
|
const theme = THEMES[themeName] || THEMES.default;
|
|
435
434
|
const contentWidth = width - 4;
|
|
436
435
|
const sampleCount = history.cpuUsage.length;
|
|
437
|
-
const sparkWidth = Math.max(10, contentWidth -
|
|
436
|
+
const sparkWidth = Math.max(10, contentWidth - 24);
|
|
438
437
|
|
|
439
438
|
const lines: string[] = [];
|
|
440
|
-
lines.push(graphRow('CPU %', history.cpuUsage, { min: 0, max: 100 }, v => `${v.toFixed(0)}%`, sparkWidth, 'collecting data...', graphMode));
|
|
441
|
-
lines.push(graphRow('Mem %', history.memUsage, { min: 0, max: 100 }, v => `${v.toFixed(0)}%`, sparkWidth, 'collecting data...', graphMode));
|
|
439
|
+
lines.push(graphRow('CPU %', history.cpuUsage, { min: 0, max: 100 }, v => `${v.toFixed(0)}%`, sparkWidth, 'collecting data...', graphMode, contentWidth));
|
|
440
|
+
lines.push(graphRow('Mem %', history.memUsage, { min: 0, max: 100 }, v => `${v.toFixed(0)}%`, sparkWidth, 'collecting data...', graphMode, contentWidth));
|
|
442
441
|
lines.push(
|
|
443
|
-
graphRow('Temp', history.temp, {},
|
|
442
|
+
graphRow('Temp', history.temp, {}, v => `${v.toFixed(1)}°C`, sparkWidth, 'no sensor access', graphMode, contentWidth)
|
|
444
443
|
);
|
|
445
|
-
lines.push(graphRow('Net RX/s', history.netRxRate, {}, v => `${formatBytes(v)}/s`, sparkWidth, 'collecting data...', graphMode));
|
|
446
|
-
lines.push(graphRow('Net TX/s', history.netTxRate, {}, v => `${formatBytes(v)}/s`, sparkWidth, 'collecting data...', graphMode));
|
|
444
|
+
lines.push(graphRow('Net RX/s', history.netRxRate, {}, v => `${formatBytes(v)}/s`, sparkWidth, 'collecting data...', graphMode, contentWidth));
|
|
445
|
+
lines.push(graphRow('Net TX/s', history.netTxRate, {}, v => `${formatBytes(v)}/s`, sparkWidth, 'collecting data...', graphMode, contentWidth));
|
|
447
446
|
if (history.powerWatts.length) {
|
|
448
|
-
lines.push(graphRow('Power W', history.powerWatts, {}, v => `${v.toFixed(1)} W`, sparkWidth, 'collecting data...', graphMode));
|
|
447
|
+
lines.push(graphRow('Power W', history.powerWatts, {}, v => `${v.toFixed(1)} W`, sparkWidth, 'collecting data...', graphMode, contentWidth));
|
|
449
448
|
}
|
|
450
449
|
if (history.rxPacketRate.length) {
|
|
451
|
-
lines.push(graphRow('RX pkt/s', history.rxPacketRate, {}, v => `${v.toFixed(0)} pkt/s`, sparkWidth, 'collecting data...', graphMode));
|
|
450
|
+
lines.push(graphRow('RX pkt/s', history.rxPacketRate, {}, v => `${v.toFixed(0)} pkt/s`, sparkWidth, 'collecting data...', graphMode, contentWidth));
|
|
452
451
|
}
|
|
453
452
|
if (history.txPacketRate.length) {
|
|
454
|
-
lines.push(graphRow('TX pkt/s', history.txPacketRate, {}, v => `${v.toFixed(0)} pkt/s`, sparkWidth, 'collecting data...', graphMode));
|
|
453
|
+
lines.push(graphRow('TX pkt/s', history.txPacketRate, {}, v => `${v.toFixed(0)} pkt/s`, sparkWidth, 'collecting data...', graphMode, contentWidth));
|
|
455
454
|
}
|
|
456
455
|
|
|
457
456
|
const title = `Graphs · mode:${graphMode} · last ${sampleCount} sample${sampleCount === 1 ? '' : 's'}`;
|
|
@@ -465,7 +464,8 @@ function graphRow(
|
|
|
465
464
|
fmt: (v: number) => string,
|
|
466
465
|
sparkWidth = 40,
|
|
467
466
|
emptyMessage = 'collecting data...',
|
|
468
|
-
mode: 'spark' | 'bar' = 'spark'
|
|
467
|
+
mode: 'spark' | 'bar' = 'spark',
|
|
468
|
+
contentWidth = 76
|
|
469
469
|
): string {
|
|
470
470
|
if (!values.length) {
|
|
471
471
|
return `${chalk.dim(label.padEnd(10))} ${chalk.dim(emptyMessage)}`;
|
|
@@ -480,7 +480,8 @@ function graphRow(
|
|
|
480
480
|
color = pct > 90 ? chalk.red : pct > 70 ? chalk.yellow : chalk.green;
|
|
481
481
|
}
|
|
482
482
|
|
|
483
|
-
|
|
483
|
+
const raw = `${chalk.dim(label.padEnd(10))}${color(spark)} ${chalk.bold(fmt(current))}`;
|
|
484
|
+
return fitVisible(raw, contentWidth);
|
|
484
485
|
}
|
|
485
486
|
|
|
486
487
|
function barGraph(values: number[], bounds: { min?: number; max?: number }, width: number): string {
|