pyre-cli 6.0.0 → 7.0.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": "6.0.0",
3
+ "version": "7.0.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",
package/src/anomalies.ts CHANGED
@@ -13,6 +13,7 @@ export interface AnomalyAlert {
13
13
  value: number;
14
14
  zScore: number;
15
15
  severity: 'warning' | 'critical';
16
+ timestamp?: Date;
16
17
  }
17
18
 
18
19
  const ZSCORE_THRESHOLD = 2.5;
@@ -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/fleet.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { execSync } from 'node:child_process';
2
+ import chalk from 'chalk';
3
+ import { formatTable } from './formatters/render.js';
4
+ import type { StatsData } from './monitors/types.js';
5
+
6
+ export function runFleetCommand(hosts: string[]) {
7
+ console.log(chalk.cyan(`Fetching data from ${hosts.length} hosts...`));
8
+ const results: { host: string; data?: StatsData; error?: string }[] = [];
9
+
10
+ for (const host of hosts) {
11
+ try {
12
+ // Connect to each host via ssh and run pyre
13
+ const out = execSync(`ssh ${host} "pyre --once --json"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
14
+ const data = JSON.parse(out) as StatsData;
15
+ results.push({ host, data });
16
+ } catch (e: any) {
17
+ results.push({ host, error: e.message });
18
+ }
19
+ }
20
+
21
+ // Very basic grid layout for fleet
22
+ for (const result of results) {
23
+ console.log(chalk.bold.blue(`\n=== ${result.host} ===\n`));
24
+ if (result.error) {
25
+ console.log(chalk.red(`Error connecting to host: ${result.error}`));
26
+ } else if (result.data) {
27
+ // Print a compact table for each host
28
+ console.log(formatTable(result.data, { width: process.stdout.columns || 80, activePanel: 'grid' }));
29
+ }
30
+ }
31
+ }
@@ -159,21 +159,118 @@ export function formatHtml(data: StatsData): string {
159
159
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
160
160
  <title>pyre — ${data.header.hostname}</title>
161
161
  <style>
162
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1e1e2e; color: #cdd6f4; margin: 0; padding: 24px; }
163
- h1 { color: #ff6a39; margin-bottom: 4px; }
164
- .meta { color: #a6adc8; margin-bottom: 24px; }
165
- h2 { color: #89b4fa; margin-top: 28px; border-bottom: 1px solid #313244; padding-bottom: 6px; }
166
- table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
167
- td { padding: 6px 10px; border-bottom: 1px solid #313244; vertical-align: top; }
168
- td.label { color: #a6adc8; width: 22%; font-weight: 500; }
169
- td.value { color: #cdd6f4; }
162
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0a0908; color: #efe9dc; margin: 0; padding: 24px; }
163
+ h1 { color: #ff6a39; margin-bottom: 4px; font-weight: 800; letter-spacing: -0.02em; }
164
+ .meta { color: #9c9384; margin-bottom: 24px; font-size: 0.9rem; }
165
+ h2 { color: #ffb130; margin-top: 28px; border-bottom: 1px solid #211d18; padding-bottom: 6px; font-size: 1.1rem; }
166
+ table { width: 100%; border-collapse: collapse; margin-bottom: 16px; font-family: monospace; }
167
+ td { padding: 8px 12px; border-bottom: 1px solid #1a1712; vertical-align: top; font-size: 0.9rem; }
168
+ td.label { color: #9c9384; width: 22%; font-weight: 600; }
169
+ td.value { color: #efe9dc; }
170
170
  tr:last-child td { border-bottom: none; }
171
171
  </style>
172
172
  </head>
173
173
  <body>
174
- <h1>PYRE</h1>
175
- <div class="meta">${escapeHtml(data.header.hostname)} · ${escapeHtml(data.header.os)} · up ${escapeHtml(data.header.uptime)} · ${escapeHtml(data.timestamp)}</div>
174
+ <h1>PYRE <span style="font-size:0.5em; color:#82c774; font-weight:normal;">● LIVE PORTAL</span></h1>
175
+ <div class="meta" id="header-meta">${escapeHtml(data.header.hostname)} · ${escapeHtml(data.header.os)} · up ${escapeHtml(data.header.uptime)} · <span id="timestamp-val">${escapeHtml(data.timestamp)}</span></div>
176
+ <div id="portal-content">
176
177
  ${sections.join('\n ')}
178
+ </div>
179
+
180
+ <script>
181
+ function renderStats(data) {
182
+ if (!data) return;
183
+ const tsEl = document.getElementById('timestamp-val');
184
+ if (tsEl && data.timestamp) tsEl.textContent = data.timestamp;
185
+
186
+ function formatBytes(bytes) {
187
+ if (!bytes || bytes < 0) return '0 B';
188
+ if (bytes < 1024) return bytes + ' B';
189
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
190
+ if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
191
+ return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
192
+ }
193
+
194
+ function row(label, val) {
195
+ return '<tr><td class="label">' + label + '</td><td class="value">' + val + '</td></tr>';
196
+ }
197
+
198
+ let html = '';
199
+
200
+ // CPU & Memory
201
+ html += '<h2>CPU & Memory</h2><table>';
202
+ if (data.cpu) {
203
+ html += row('Brand', data.cpu.brand || 'Unknown');
204
+ html += row('Cores', (data.cpu.physicalCores || '') + '/' + (data.cpu.cores || '') + ' (phys/log)');
205
+ html += row('Usage', (data.cpu.usage || 0) + '%');
206
+ if (data.cpu.loadAvg) html += row('Load Avg', data.cpu.loadAvg.map(l => l.toFixed(2)).join(' '));
207
+ if (data.cpu.temperature) html += row('Temperature', data.cpu.temperature.toFixed(1) + '°C');
208
+ }
209
+ if (data.memory) {
210
+ html += row('Memory Usage', data.memory.usagePercent + '% (' + formatBytes(data.memory.used) + ' / ' + formatBytes(data.memory.total) + ')');
211
+ html += row('Swap Used', formatBytes(data.memory.swapUsed) + ' / ' + formatBytes(data.memory.swapTotal));
212
+ }
213
+ html += '</table>';
214
+
215
+ // GPU
216
+ if (data.gpu) {
217
+ html += '<h2>GPU</h2><table>';
218
+ html += row('Model', data.gpu.model || '');
219
+ html += row('Utilization', (data.gpu.utilization || 0) + '%');
220
+ html += row('Memory', formatBytes(data.gpu.memory));
221
+ if (data.gpu.temperature) html += row('Temperature', data.gpu.temperature.toFixed(1) + '°C');
222
+ html += '</table>';
223
+ }
224
+
225
+ // Network
226
+ if (data.network) {
227
+ html += '<h2>Network</h2><table>';
228
+ html += row('Interface', (data.network.interface || '') + ' (' + (data.network.ip || '') + ')');
229
+ html += row('RX', formatBytes(data.network.rxBytes) + ' / ' + (data.network.rxPackets || 0) + ' pkt');
230
+ html += row('TX', formatBytes(data.network.txBytes) + ' / ' + (data.network.txPackets || 0) + ' pkt');
231
+ html += '</table>';
232
+ }
233
+
234
+ // Disk
235
+ if (data.disk && data.disk.length) {
236
+ html += '<h2>Disk</h2><table>';
237
+ data.disk.forEach(d => {
238
+ let io = '';
239
+ if (d.readBytesSec !== undefined || d.writeBytesSec !== undefined) {
240
+ io = ' | R: ' + formatBytes(d.readBytesSec || 0) + '/s W: ' + formatBytes(d.writeBytesSec || 0) + '/s';
241
+ }
242
+ html += row(d.mountpoint, d.used + ' / ' + d.size + ' (' + d.capacity + ')' + io);
243
+ });
244
+ html += '</table>';
245
+ }
246
+
247
+ // Processes
248
+ if (data.processes && data.processes.length) {
249
+ html += '<h2>Processes (Top ' + Math.min(50, data.processes.length) + ')</h2><table>';
250
+ data.processes.slice(0, 50).forEach(p => {
251
+ html += row(String(p.pid), p.user + ' &nbsp; CPU: ' + p.cpu.toFixed(1) + '% &nbsp; MEM: ' + p.mem.toFixed(1) + '% &nbsp; ' + p.command);
252
+ });
253
+ html += '</table>';
254
+ }
255
+
256
+ const contentEl = document.getElementById('portal-content');
257
+ if (contentEl) contentEl.innerHTML = html;
258
+ }
259
+
260
+ if (window.EventSource) {
261
+ const evtSource = new EventSource('/api/stream');
262
+ evtSource.onmessage = function(event) {
263
+ try {
264
+ const data = JSON.parse(event.data);
265
+ renderStats(data);
266
+ } catch(e) {}
267
+ };
268
+ } else {
269
+ setInterval(function() {
270
+ fetch('/api').then(r => r.json()).then(renderStats).catch(function(){});
271
+ }, 2000);
272
+ }
273
+ </script>
177
274
  </body>
178
275
  </html>`;
179
276
  }