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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyre-cli",
3
- "version": "5.1.0",
3
+ "version": "6.1.0",
4
4
  "description": "CLI Mac monitoring tool: temps, system stats, live dashboard, packet monitor, battery predictor, export",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -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
+ }
@@ -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';
@@ -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 - 28);
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, {}, formatTemp, sparkWidth, 'no sensor access (needs sudo powermetrics)', graphMode)
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
- return `${chalk.dim(label.padEnd(10))}${color(spark)} ${chalk.bold(fmt(current))}`;
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 {