pyre-cli 7.0.0 → 8.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/README.md +42 -19
- package/dist/anomaliesCmd-TO52YY2B.js +117 -0
- package/dist/brewHealth-6NE37NAU.js +74 -0
- package/dist/chunk-GLH7EKAX.js +15 -0
- package/dist/chunk-KBKXU2ZV.js +859 -0
- package/dist/chunk-RVGKEJXT.js +121 -0
- package/dist/collectors-4VYRJVRO.js +33 -0
- package/dist/completions-VQMHQCXE.js +122 -0
- package/dist/config-4ZLF4HTS.js +22 -0
- package/dist/doctor-LJQAABZ4.js +184 -0
- package/dist/extensions-QCFSVCL5.js +74 -0
- package/dist/index.js +1281 -1193
- package/dist/updateCheck-52X6LJG5.js +84 -0
- package/package.json +1 -1
- package/parity.md +16 -9
- package/src/anomaliesCmd.ts +151 -0
- package/src/brewHealth.ts +88 -0
- package/src/doctor.ts +77 -1
- package/src/easy_features.test.ts +43 -0
- package/src/extensions.ts +83 -0
- package/src/formatters/html_template.ts +655 -0
- package/src/formatters/index.ts +1 -0
- package/src/formatters/render.ts +2 -2
- package/src/index.ts +280 -26
- package/src/live/input.ts +14 -2
- package/src/monitors/collectors.ts +110 -5
- package/src/monitors/types.ts +6 -0
- package/src/sparkline.ts +63 -2
- package/src/state/config.ts +36 -2
- package/src/updateCheck.ts +94 -0
package/src/formatters/render.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* all layout-related code is co-located.
|
|
11
11
|
*/
|
|
12
12
|
import chalk from 'chalk';
|
|
13
|
-
import { sparkline, multiRowSparkline, brailleGraph } from '../sparkline.js';
|
|
13
|
+
import { sparkline, multiRowSparkline, brailleGraph, multiRowBrailleGraph } from '../sparkline.js';
|
|
14
14
|
import type { History } from '../history.js';
|
|
15
15
|
import type { StatsData, VisibleItems, TableOptions, AnomalyAlert } from './types.js';
|
|
16
16
|
import { THEMES, type ThemeName, type ThemeColors } from './themes.js';
|
|
@@ -730,7 +730,7 @@ function btopCpuBox(data: StatsData, width: number, theme: ThemeColors, opts: Ta
|
|
|
730
730
|
|
|
731
731
|
const cpuHistory = opts.history?.cpuUsage || [];
|
|
732
732
|
if (cpuHistory.length > 1) {
|
|
733
|
-
const graphRows =
|
|
733
|
+
const graphRows = multiRowBrailleGraph(cpuHistory, 3, { min: 0, max: 100 });
|
|
734
734
|
for (const gr of graphRows) {
|
|
735
735
|
leftLines.push(theme.cpu(fitVisible(gr, leftW)));
|
|
736
736
|
}
|
package/src/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ import readline from 'node:readline';
|
|
|
13
13
|
import { Command } from 'commander';
|
|
14
14
|
import chalk from 'chalk';
|
|
15
15
|
import { collectAll } from './monitors/index.js';
|
|
16
|
-
import { formatTable, formatJson, formatCsv, formatTsv, formatHtml, formatMarkdown, formatBytes } from './formatters/index.js';
|
|
16
|
+
import { formatTable, formatJson, formatCsv, formatTsv, formatHtml, getDashboardHtml, formatMarkdown, formatBytes } from './formatters/index.js';
|
|
17
17
|
import { startLive, stopLive } from './live/index.js';
|
|
18
18
|
import { showSplash } from './splash.js';
|
|
19
19
|
import { P2PServer, P2PClient } from './p2p/index.js';
|
|
@@ -67,6 +67,9 @@ program
|
|
|
67
67
|
.option('--p2p-deny <ips>', 'Comma-separated list of denied IPs')
|
|
68
68
|
.option('--p2p-audit-log <dir>', 'Directory for P2P audit logs')
|
|
69
69
|
.option('--p2p-hmac-key <key>', 'HMAC key for message signing (default: derived from password)')
|
|
70
|
+
.option('--since <range>', 'Time range for anomalies digest (e.g. 1d, 7d, yesterday)')
|
|
71
|
+
.option('--plain', 'Output in plain accessible text without ANSI colors or line-drawing characters')
|
|
72
|
+
.option('--a11y', 'Alias for --plain')
|
|
70
73
|
.option('--webhook-url <url>', 'URL to POST to on alert')
|
|
71
74
|
.option('--alert-cmd <cmd>', 'Command to execute on alert')
|
|
72
75
|
.option('--port <port>', 'Port number for web server mode', '3000');
|
|
@@ -75,6 +78,10 @@ program.parse(process.argv);
|
|
|
75
78
|
|
|
76
79
|
const opts = program.opts();
|
|
77
80
|
|
|
81
|
+
if (opts.plain || opts.a11y) {
|
|
82
|
+
chalk.level = 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
78
85
|
function isExportMode() {
|
|
79
86
|
return opts.json || opts.csv || opts.tsv || opts.html || opts.md;
|
|
80
87
|
}
|
|
@@ -145,6 +152,61 @@ async function runServerCommand(): Promise<void> {
|
|
|
145
152
|
async function main() {
|
|
146
153
|
const cmd = program.args[0];
|
|
147
154
|
|
|
155
|
+
if (cmd === 'anomalies') {
|
|
156
|
+
const { runAnomaliesDigest } = await import('./anomaliesCmd.js');
|
|
157
|
+
runAnomaliesDigest({ since: opts.since || '7d', dir: opts.exportDir });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (cmd === 'extensions') {
|
|
162
|
+
const { printExtensionsReport } = await import('./extensions.js');
|
|
163
|
+
await printExtensionsReport();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (cmd === 'brew') {
|
|
168
|
+
const { printBrewHealthReport } = await import('./brewHealth.js');
|
|
169
|
+
await printBrewHealthReport();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (cmd === 'update') {
|
|
174
|
+
const { printUpdateReport } = await import('./updateCheck.js');
|
|
175
|
+
await printUpdateReport(pkg.version);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (cmd === 'profile') {
|
|
180
|
+
const sub = program.args[1];
|
|
181
|
+
const name = program.args[2];
|
|
182
|
+
const { saveProfile, loadProfile, listProfiles } = await import('./state/config.js');
|
|
183
|
+
if (sub === 'save' && name) {
|
|
184
|
+
saveProfile(name);
|
|
185
|
+
console.log(chalk.green(` ✔ Profile '${name}' saved successfully.`));
|
|
186
|
+
} else if (sub === 'load' && name) {
|
|
187
|
+
try {
|
|
188
|
+
loadProfile(name);
|
|
189
|
+
console.log(chalk.green(` ✔ Profile '${name}' loaded successfully.`));
|
|
190
|
+
} catch (err: any) {
|
|
191
|
+
console.log(chalk.red(` ✖ ${err.message}`));
|
|
192
|
+
}
|
|
193
|
+
} else if (sub === 'list') {
|
|
194
|
+
const profiles = listProfiles();
|
|
195
|
+
console.log(chalk.bold('\n pyre Profiles:\n'));
|
|
196
|
+
if (profiles.length === 0) {
|
|
197
|
+
console.log(chalk.dim(' No saved profiles found. Save one with `pyre profile save <name>`.'));
|
|
198
|
+
} else {
|
|
199
|
+
for (const p of profiles) {
|
|
200
|
+
console.log(` • ${p}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
console.log();
|
|
204
|
+
} else {
|
|
205
|
+
console.log('Usage: pyre profile <save|load|list> [name]');
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
148
210
|
if (cmd === 'history') {
|
|
149
211
|
const daysIdx = program.args.indexOf('--days');
|
|
150
212
|
const days = daysIdx !== -1 ? parseInt(program.args[daysIdx + 1], 10) || 7 : 7;
|
|
@@ -178,6 +240,11 @@ async function main() {
|
|
|
178
240
|
return;
|
|
179
241
|
}
|
|
180
242
|
|
|
243
|
+
if (cmd === 'ui') {
|
|
244
|
+
await runUiCommand();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
181
248
|
if (cmd === 'bench') {
|
|
182
249
|
const benchCmd = program.args.slice(1).join(' ');
|
|
183
250
|
if (!benchCmd) {
|
|
@@ -212,6 +279,25 @@ async function main() {
|
|
|
212
279
|
return;
|
|
213
280
|
}
|
|
214
281
|
|
|
282
|
+
if (cmd === 'doctor') {
|
|
283
|
+
const { runDoctor, printDoctorReport } = await import('./doctor.js');
|
|
284
|
+
const checks = await runDoctor();
|
|
285
|
+
printDoctorReport(checks);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (cmd === 'info') {
|
|
290
|
+
await runInfoCommand();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (cmd === 'completions') {
|
|
295
|
+
const { printCompletions } = await import('./completions.js');
|
|
296
|
+
const shell = program.args[1] || 'zsh';
|
|
297
|
+
printCompletions(shell);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
215
301
|
if (cmd === 'fleet') {
|
|
216
302
|
const hosts = program.args.slice(1);
|
|
217
303
|
if (hosts.length === 0) {
|
|
@@ -492,8 +578,12 @@ async function runP2PConnect(): Promise<void> {
|
|
|
492
578
|
|
|
493
579
|
async function runInfoCommand(): Promise<void> {
|
|
494
580
|
const data = await collectAll({ detailed: true });
|
|
581
|
+
const { getDisplayInfo, getTimeMachineStatus } = await import('./monitors/collectors.js');
|
|
582
|
+
const displays = await getDisplayInfo();
|
|
583
|
+
const tmStatus = await getTimeMachineStatus();
|
|
584
|
+
|
|
495
585
|
const lines: string[] = [];
|
|
496
|
-
lines.push(chalk.bold(' pyre info'));
|
|
586
|
+
lines.push(chalk.bold(' pyre info — Mac System Information'));
|
|
497
587
|
lines.push('');
|
|
498
588
|
lines.push(` Hostname: ${data.header.hostname}`);
|
|
499
589
|
lines.push(` OS: ${data.header.os}`);
|
|
@@ -501,13 +591,34 @@ async function runInfoCommand(): Promise<void> {
|
|
|
501
591
|
lines.push(` CPU: ${data.cpu.brand}`);
|
|
502
592
|
lines.push(` Cores: ${data.cpu.physicalCores}/${data.cpu.cores} (phys/log)`);
|
|
503
593
|
lines.push(` Frequency: ${data.cpu.frequency} MHz`);
|
|
504
|
-
lines.push(` Memory: ${formatBytes(data.memory.total)}`);
|
|
594
|
+
lines.push(` Memory: ${formatBytes(data.memory.total)}${data.memory.pressureLevel ? ` (Pressure: ${data.memory.pressureLevel})` : ''}`);
|
|
595
|
+
if (data.memory.wiredBytes !== undefined && data.memory.compressedBytes !== undefined) {
|
|
596
|
+
lines.push(` Wired: ${formatBytes(data.memory.wiredBytes)} | Compressed: ${formatBytes(data.memory.compressedBytes)}`);
|
|
597
|
+
}
|
|
505
598
|
if (data.gpu) lines.push(` GPU: ${data.gpu.model} (${formatBytes(data.gpu.memory)})`);
|
|
506
599
|
if (data.battery) {
|
|
507
600
|
lines.push(` Battery: ${data.battery.level}% (${data.battery.state})`);
|
|
508
601
|
lines.push(` Health: ${data.battery.health}`);
|
|
509
602
|
}
|
|
510
603
|
lines.push(` Thermal: ${data.thermal.state}`);
|
|
604
|
+
|
|
605
|
+
if (displays.length > 0) {
|
|
606
|
+
lines.push('');
|
|
607
|
+
lines.push(chalk.cyan.bold(' 🖥 Displays:'));
|
|
608
|
+
for (const d of displays) {
|
|
609
|
+
lines.push(` • ${d.name}: ${d.resolution}${d.isMain ? ' (Main)' : ''}${d.connectionType ? ` [${d.connectionType}]` : ''}`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
lines.push('');
|
|
614
|
+
lines.push(chalk.cyan.bold(' ⏱ Time Machine Backup:'));
|
|
615
|
+
if (!tmStatus.configured) {
|
|
616
|
+
lines.push(' • Not configured');
|
|
617
|
+
} else {
|
|
618
|
+
lines.push(` • Status: ${tmStatus.running ? chalk.yellow(`Backup in progress (${tmStatus.percent || 0}%)`) : chalk.green('Idle')}`);
|
|
619
|
+
lines.push(` • Latest Backup: ${tmStatus.latestBackup}`);
|
|
620
|
+
}
|
|
621
|
+
|
|
511
622
|
console.log(lines.join('\n'));
|
|
512
623
|
}
|
|
513
624
|
|
|
@@ -541,9 +652,9 @@ async function runSshCommand(host: string): Promise<void> {
|
|
|
541
652
|
});
|
|
542
653
|
}
|
|
543
654
|
|
|
544
|
-
async function runWebCommand(): Promise<
|
|
655
|
+
async function runWebCommand(): Promise<number> {
|
|
545
656
|
const http = await import('node:http');
|
|
546
|
-
const port = parseInt(opts.port || process.env.PORT || '3000', 10) || 3000;
|
|
657
|
+
const port = parseInt(opts.port || process.env.PORT || '3000', 10) || (opts.port === '0' ? 0 : 3000);
|
|
547
658
|
|
|
548
659
|
const server = http.createServer(async (req, res) => {
|
|
549
660
|
const url = req.url || '/';
|
|
@@ -585,17 +696,14 @@ async function runWebCommand(): Promise<void> {
|
|
|
585
696
|
// 4. Static/Dynamic HTML live dashboard & API snapshot endpoints
|
|
586
697
|
if (url === '/' || url === '/index.html') {
|
|
587
698
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
588
|
-
|
|
589
|
-
const htmlContent = formatHtml(data);
|
|
590
|
-
res.end(htmlContent);
|
|
699
|
+
res.end(getDashboardHtml());
|
|
591
700
|
} else if (url === '/setup') {
|
|
592
701
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
593
702
|
const indexPath = path.join(process.cwd(), 'index.html');
|
|
594
703
|
if (fs.existsSync(indexPath)) {
|
|
595
704
|
res.end(fs.readFileSync(indexPath, 'utf-8'));
|
|
596
705
|
} else {
|
|
597
|
-
|
|
598
|
-
res.end(formatHtml(data));
|
|
706
|
+
res.end(getDashboardHtml());
|
|
599
707
|
}
|
|
600
708
|
} else if (url === '/api' || url === '/api/stats' || url === '/data') {
|
|
601
709
|
const data = await collectAll({ detailed: true });
|
|
@@ -625,20 +733,145 @@ async function runWebCommand(): Promise<void> {
|
|
|
625
733
|
|
|
626
734
|
const ip = detectLocalIP() || 'localhost';
|
|
627
735
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
736
|
+
return new Promise((resolve) => {
|
|
737
|
+
server.listen(port, '0.0.0.0', () => {
|
|
738
|
+
const address = server.address();
|
|
739
|
+
const actualPort = typeof address === 'object' && address ? address.port : port;
|
|
740
|
+
|
|
741
|
+
console.log(chalk.bold(`\n pyre web server running on port ${actualPort}`));
|
|
742
|
+
console.log(chalk.dim(` Local URL: http://localhost:${actualPort}/`));
|
|
743
|
+
console.log(chalk.hex('#ff6a39').bold(` Network URL: http://${ip}:${actualPort}/ (Accessible on same Wi-Fi / LAN)`));
|
|
744
|
+
console.log(chalk.dim(` API: http://${ip}:${actualPort}/api`));
|
|
745
|
+
console.log(chalk.dim(` SSE Stream: http://${ip}:${actualPort}/api/stream`));
|
|
746
|
+
|
|
747
|
+
resolve(actualPort);
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
process.once('SIGINT', () => {
|
|
751
|
+
server.close();
|
|
752
|
+
process.exit(0);
|
|
753
|
+
});
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
async function runUiCommand(): Promise<void> {
|
|
758
|
+
// Use a random ephemeral port to avoid collisions
|
|
759
|
+
opts.port = '0';
|
|
760
|
+
const port = await runWebCommand();
|
|
761
|
+
|
|
762
|
+
const swiftScript = `
|
|
763
|
+
import Cocoa
|
|
764
|
+
import WebKit
|
|
765
|
+
|
|
766
|
+
// WKWebView subclass that allows the window to be dragged from any pixel
|
|
767
|
+
class DraggableWebView: WKWebView {
|
|
768
|
+
override var mouseDownCanMoveWindow: Bool { return true }
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
|
772
|
+
var window: NSWindow!
|
|
773
|
+
var webView: DraggableWebView!
|
|
774
|
+
var dataTimer: Timer?
|
|
775
|
+
let serverPort = ${port}
|
|
776
|
+
|
|
777
|
+
func applicationDidFinishLaunching(_ n: Notification) {
|
|
778
|
+
// Window
|
|
779
|
+
let screen = NSScreen.main?.visibleFrame ?? NSRect(x:0, y:0, width:1280, height:800)
|
|
780
|
+
let W = min(1180.0, screen.width - 80)
|
|
781
|
+
let H = min(820.0, screen.height - 80)
|
|
782
|
+
let rect = NSRect(x: screen.minX + (screen.width - W) / 2,
|
|
783
|
+
y: screen.minY + (screen.height - H) / 2,
|
|
784
|
+
width: W, height: H)
|
|
785
|
+
|
|
786
|
+
window = NSWindow(contentRect: rect,
|
|
787
|
+
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
|
788
|
+
backing: .buffered, defer: false)
|
|
789
|
+
window.title = "Pyre"
|
|
790
|
+
window.minSize = NSSize(width: 800, height: 600)
|
|
791
|
+
window.appearance = NSAppearance(named: .darkAqua)
|
|
792
|
+
window.backgroundColor = NSColor(white: 0, alpha: 1)
|
|
793
|
+
window.isReleasedWhenClosed = false
|
|
794
|
+
window.delegate = self
|
|
795
|
+
|
|
796
|
+
// WebView
|
|
797
|
+
let cfg = WKWebViewConfiguration()
|
|
798
|
+
webView = DraggableWebView(frame: window.contentView!.bounds, configuration: cfg)
|
|
799
|
+
webView.autoresizingMask = [.width, .height]
|
|
800
|
+
webView.setValue(NSColor(white: 0, alpha: 1),
|
|
801
|
+
forKey: "backgroundColor")
|
|
802
|
+
|
|
803
|
+
// Load the dashboard HTML from the local server
|
|
804
|
+
var req = URLRequest(url: URL(string: "http://localhost:\\(serverPort)/")!)
|
|
805
|
+
req.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
|
|
806
|
+
webView.load(req)
|
|
807
|
+
|
|
808
|
+
window.contentView?.addSubview(webView)
|
|
809
|
+
window.makeKeyAndOrderFront(nil)
|
|
810
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
811
|
+
|
|
812
|
+
// Start native data timer (URLSession has full network access — no sandbox issues)
|
|
813
|
+
// We wait 1.5 s for the page to load, then start pushing data every 2 s.
|
|
814
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
|
|
815
|
+
self?.fetchAndPush()
|
|
816
|
+
self?.dataTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in
|
|
817
|
+
self?.fetchAndPush()
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// URLSession fetch (runs in the main app process — not sandboxed like WebContent)
|
|
823
|
+
func fetchAndPush() {
|
|
824
|
+
guard let url = URL(string: "http://localhost:\\(serverPort)/api/stats") else { return }
|
|
825
|
+
URLSession.shared.dataTask(with: url) { [weak self] data, _, err in
|
|
826
|
+
if let err = err {
|
|
827
|
+
print("Swift UI Fetch Error: \\(err)")
|
|
828
|
+
return
|
|
829
|
+
}
|
|
830
|
+
guard let data = data else {
|
|
831
|
+
print("Swift UI Fetch Error: No data")
|
|
832
|
+
return
|
|
833
|
+
}
|
|
834
|
+
let b64 = data.base64EncodedString()
|
|
835
|
+
let js = "if(window.__pyreUpdate){window.__pyreUpdate(decodeURIComponent(escape(atob('\\(b64)'))))}"
|
|
836
|
+
DispatchQueue.main.async {
|
|
837
|
+
self?.webView.evaluateJavaScript(js) { res, err in
|
|
838
|
+
if let err = err {
|
|
839
|
+
print("Swift UI JS Eval Error: \\(err)")
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}.resume()
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
func windowWillClose(_ n: Notification) {
|
|
847
|
+
dataTimer?.invalidate()
|
|
848
|
+
NSApplication.shared.terminate(self)
|
|
849
|
+
}
|
|
850
|
+
func applicationShouldTerminateAfterLastWindowClosed(_ s: NSApplication) -> Bool { true }
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
let app = NSApplication.shared
|
|
854
|
+
app.setActivationPolicy(.regular)
|
|
855
|
+
let delegate = AppDelegate()
|
|
856
|
+
app.delegate = delegate
|
|
857
|
+
app.run()
|
|
858
|
+
`;
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
const scriptPath = path.join(os.tmpdir(), 'pyre-ui.swift');
|
|
862
|
+
fs.writeFileSync(scriptPath, swiftScript);
|
|
863
|
+
|
|
864
|
+
const { spawn } = await import('node:child_process');
|
|
865
|
+
console.log(chalk.cyan(`\n Launching native UI window...`));
|
|
866
|
+
const ui = spawn('swift', [scriptPath], { stdio: 'inherit' });
|
|
867
|
+
|
|
868
|
+
ui.on('close', () => {
|
|
869
|
+
console.log(chalk.dim('UI window closed, shutting down server...'));
|
|
870
|
+
process.exit(0);
|
|
638
871
|
});
|
|
639
872
|
|
|
640
873
|
process.once('SIGINT', () => {
|
|
641
|
-
|
|
874
|
+
ui.kill();
|
|
642
875
|
process.exit(0);
|
|
643
876
|
});
|
|
644
877
|
}
|
|
@@ -650,12 +883,16 @@ async function runBenchCommand(benchCmd: string): Promise<void> {
|
|
|
650
883
|
|
|
651
884
|
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
|
|
652
885
|
const stream = fs.createWriteStream(file, { flags: 'a' });
|
|
653
|
-
stream.write('timestamp,cpu_usage,mem_usage_percent,temp_c,net_rx_bytes,net_tx_bytes,net_rx_packets,net_tx_packets,connections,thermal_state\n');
|
|
886
|
+
stream.write('timestamp,cpu_usage,mem_usage_percent,temp_c,net_rx_bytes,net_tx_bytes,net_rx_packets,net_tx_packets,connections,thermal_state,power_watts\n');
|
|
654
887
|
|
|
655
888
|
console.log(chalk.bold(`\n pyre bench`));
|
|
656
889
|
console.log(chalk.dim(` Command: ${benchCmd}`));
|
|
657
890
|
console.log(chalk.dim(` Log: ${file}\n`));
|
|
658
891
|
|
|
892
|
+
const startTime = Date.now();
|
|
893
|
+
let accumulatedWattSeconds = 0;
|
|
894
|
+
let powerSamplesCount = 0;
|
|
895
|
+
|
|
659
896
|
const child = spawn('sh', ['-c', benchCmd]);
|
|
660
897
|
child.stdout.on('data', d => process.stdout.write(d));
|
|
661
898
|
child.stderr.on('data', d => process.stdout.write(d));
|
|
@@ -668,23 +905,40 @@ async function runBenchCommand(benchCmd: string): Promise<void> {
|
|
|
668
905
|
const rxPackets = data.network.rxPackets ?? 0;
|
|
669
906
|
const txPackets = data.network.txPackets ?? 0;
|
|
670
907
|
const connections = data.network.connections ?? 0;
|
|
671
|
-
|
|
908
|
+
const watts = data.power?.combinedWatts ?? data.power?.cpuWatts;
|
|
909
|
+
if (watts !== undefined && watts > 0) {
|
|
910
|
+
accumulatedWattSeconds += watts * interval;
|
|
911
|
+
powerSamplesCount++;
|
|
912
|
+
}
|
|
913
|
+
stream.write(`${data.timestamp},${data.cpu.usage},${data.memory.usagePercent},${temp},${data.network.rxBytes},${data.network.txBytes},${rxPackets},${txPackets},${connections},${data.thermal.state},${watts ?? ''}\n`);
|
|
672
914
|
} catch {
|
|
673
915
|
// skip bad tick
|
|
674
916
|
}
|
|
675
917
|
}, interval * 1000);
|
|
676
918
|
|
|
677
|
-
|
|
919
|
+
const printSummary = () => {
|
|
678
920
|
clearInterval(handle);
|
|
679
921
|
stream.end();
|
|
680
|
-
|
|
922
|
+
const durationSec = Math.max(0.1, (Date.now() - startTime) / 1000);
|
|
923
|
+
console.log(chalk.green(`\n ✔ Bench complete (${durationSec.toFixed(2)}s). Log saved to ${file}`));
|
|
924
|
+
|
|
925
|
+
if (powerSamplesCount > 0 && accumulatedWattSeconds > 0) {
|
|
926
|
+
const kWh = accumulatedWattSeconds / (3600 * 1000);
|
|
927
|
+
const rate = config.electricityRateKwH ?? 0.15;
|
|
928
|
+
const cost = kWh * rate;
|
|
929
|
+
console.log(chalk.cyan(` ⚡ Energy Consumption: ${kWh.toFixed(6)} kWh | Est. Cost: $${cost.toFixed(6)} (@ $${rate}/kWh)`));
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
child.on('close', () => {
|
|
934
|
+
printSummary();
|
|
681
935
|
process.exit(0);
|
|
682
936
|
});
|
|
683
937
|
|
|
684
938
|
process.once('SIGINT', () => {
|
|
685
939
|
clearInterval(handle);
|
|
686
940
|
child.kill('SIGTERM');
|
|
687
|
-
|
|
941
|
+
printSummary();
|
|
688
942
|
process.exit(0);
|
|
689
943
|
});
|
|
690
944
|
}
|
package/src/live/input.ts
CHANGED
|
@@ -42,10 +42,10 @@ import { writeConfig } from '../state/config.js';
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function isUpKey(key: readline.Key, str?: string): boolean {
|
|
45
|
-
return key.name === 'up' ||
|
|
45
|
+
return key.name === 'up' || key.sequence === '\x1b[A' || key.sequence === '\x1bOA';
|
|
46
46
|
}
|
|
47
47
|
function isDownKey(key: readline.Key, str?: string): boolean {
|
|
48
|
-
return key.name === 'down' ||
|
|
48
|
+
return key.name === 'down' || key.sequence === '\x1b[B' || key.sequence === '\x1bOB';
|
|
49
49
|
}
|
|
50
50
|
function isEnterKey(key: readline.Key, str?: string): boolean {
|
|
51
51
|
return key.name === 'return' || key.name === 'enter' || key.sequence === '\r' || key.sequence === '\n' || str === '\r' || str === '\n';
|
|
@@ -207,12 +207,24 @@ function handleInputModeKey(str: string, key: readline.Key) {
|
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
let confirmKillPid: number | null = null;
|
|
211
|
+
|
|
210
212
|
function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
211
213
|
const pid = parseInt(pidStr, 10);
|
|
212
214
|
if (!pid || pid <= 0) {
|
|
213
215
|
setStatus(`Invalid PID: ${pidStr}`);
|
|
214
216
|
return;
|
|
215
217
|
}
|
|
218
|
+
|
|
219
|
+
const isProtected = pid === 1 || pid === process.pid || pid === process.ppid;
|
|
220
|
+
if (isProtected && confirmKillPid !== pid) {
|
|
221
|
+
confirmKillPid = pid;
|
|
222
|
+
const name = pid === 1 ? 'launchd (system init)' : (pid === process.pid ? 'pyre itself' : 'parent process');
|
|
223
|
+
setStatus(`⚠️ PROTECTED PID ${pid} (${name})! Re-enter PID ${pid} to confirm termination.`, 8000);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
confirmKillPid = null;
|
|
216
228
|
try {
|
|
217
229
|
process.kill(pid, signal as NodeJS.Signals);
|
|
218
230
|
setStatus(`Sent ${signal} to PID ${pid}`);
|
|
@@ -466,6 +466,14 @@ export async function collectMemory(): Promise<MemoryData> {
|
|
|
466
466
|
const wiredPages = getPageCount('wired down');
|
|
467
467
|
const compressedPages = getPageCount('occupied by compressor');
|
|
468
468
|
|
|
469
|
+
const purgeablePages = getPageCount('purgeable');
|
|
470
|
+
const getStatCount = (label: string) => {
|
|
471
|
+
const m = vmStat.match(new RegExp(`${label}:\\s+(\\d+)\\.`));
|
|
472
|
+
return m ? parseInt(m[1]) : 0;
|
|
473
|
+
};
|
|
474
|
+
const swapIns = getStatCount('Swapins') || getStatCount('Pageins');
|
|
475
|
+
const swapOuts = getStatCount('Swapouts') || getStatCount('Pageouts');
|
|
476
|
+
|
|
469
477
|
const free = freePages * pageSize;
|
|
470
478
|
const used = (activePages + inactivePages + wiredPages + compressedPages) * pageSize;
|
|
471
479
|
const usagePercent = totalBytes > 0 ? Math.round((used / totalBytes) * 100) : 0;
|
|
@@ -475,11 +483,6 @@ export async function collectMemory(): Promise<MemoryData> {
|
|
|
475
483
|
let swapFree = 0;
|
|
476
484
|
try {
|
|
477
485
|
const swapRaw = (await run('sysctl -n vm.swapusage')).trim();
|
|
478
|
-
// Real output is "total = 3072.00M used = 1497.75M free = 1574.25M
|
|
479
|
-
// (encrypted)" — the parentheses wrap the trailing "(encrypted)" note,
|
|
480
|
-
// not the used value. The old regex required a literal "(" right before
|
|
481
|
-
// "used", which never appears, so this never matched and swap stayed at
|
|
482
|
-
// 0 forever instead of updating.
|
|
483
486
|
const swapMatch = swapRaw.match(
|
|
484
487
|
/total\s*=\s*([\d.]+)([KMGkmg])\s+used\s*=\s*([\d.]+)([KMGkmg])\s+free\s*=\s*([\d.]+)([KMGkmg])/
|
|
485
488
|
);
|
|
@@ -492,6 +495,18 @@ export async function collectMemory(): Promise<MemoryData> {
|
|
|
492
495
|
// ignore
|
|
493
496
|
}
|
|
494
497
|
|
|
498
|
+
let pressureLevel = 'Normal';
|
|
499
|
+
try {
|
|
500
|
+
const pressVal = (await run('sysctl -n kern.memorystatus_vm_pressure_level')).trim();
|
|
501
|
+
const pressNum = parseInt(pressVal, 10);
|
|
502
|
+
if (pressNum === 2) pressureLevel = 'Warning';
|
|
503
|
+
else if (pressNum >= 4) pressureLevel = 'Critical';
|
|
504
|
+
else if (!isNaN(pressNum)) pressureLevel = 'Normal';
|
|
505
|
+
else pressureLevel = pressVal || 'Normal';
|
|
506
|
+
} catch {
|
|
507
|
+
pressureLevel = 'Normal';
|
|
508
|
+
}
|
|
509
|
+
|
|
495
510
|
return {
|
|
496
511
|
total: totalBytes,
|
|
497
512
|
used,
|
|
@@ -501,6 +516,12 @@ export async function collectMemory(): Promise<MemoryData> {
|
|
|
501
516
|
swapFree,
|
|
502
517
|
pageSize,
|
|
503
518
|
usagePercent,
|
|
519
|
+
wiredBytes: wiredPages * pageSize,
|
|
520
|
+
compressedBytes: compressedPages * pageSize,
|
|
521
|
+
purgeableBytes: purgeablePages * pageSize,
|
|
522
|
+
swapIns,
|
|
523
|
+
swapOuts,
|
|
524
|
+
pressureLevel,
|
|
504
525
|
};
|
|
505
526
|
}
|
|
506
527
|
|
|
@@ -855,7 +876,91 @@ export async function collectTasks(limit = 12): Promise<TaskData[]> {
|
|
|
855
876
|
return { pid: parseInt(parts[1]), user: parts[2], cpu: parseFloat(parts[3]), mem: parseFloat(parts[4]), state: parts[5], runtime: runtimeSec, command: parts[7] };
|
|
856
877
|
})
|
|
857
878
|
.filter((t): t is TaskData => t !== null && t.pid > 0);
|
|
879
|
+
} catch {
|
|
880
|
+
return [];
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
export interface DisplayInfo {
|
|
885
|
+
name: string;
|
|
886
|
+
resolution: string;
|
|
887
|
+
connectionType?: string;
|
|
888
|
+
isMain?: boolean;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
export async function getDisplayInfo(): Promise<DisplayInfo[]> {
|
|
892
|
+
try {
|
|
893
|
+
const raw = await run('system_profiler SPDisplaysDataType 2>&1', '');
|
|
894
|
+
const displays: DisplayInfo[] = [];
|
|
895
|
+
const lines = raw.split('\n');
|
|
896
|
+
let currentName = '';
|
|
897
|
+
let currentRes = '';
|
|
898
|
+
let isMain = false;
|
|
899
|
+
let connType = '';
|
|
900
|
+
|
|
901
|
+
for (const line of lines) {
|
|
902
|
+
const trimmed = line.trim();
|
|
903
|
+
if (trimmed.endsWith(':') && !trimmed.startsWith('Chipset') && !trimmed.startsWith('Displays') && !trimmed.startsWith('Graphics')) {
|
|
904
|
+
if (currentName && currentRes) {
|
|
905
|
+
displays.push({ name: currentName, resolution: currentRes, isMain, connectionType: connType || undefined });
|
|
906
|
+
currentRes = '';
|
|
907
|
+
isMain = false;
|
|
908
|
+
connType = '';
|
|
909
|
+
}
|
|
910
|
+
currentName = trimmed.slice(0, -1);
|
|
911
|
+
} else if (trimmed.startsWith('Resolution:')) {
|
|
912
|
+
currentRes = trimmed.replace('Resolution:', '').trim();
|
|
913
|
+
} else if (trimmed.startsWith('Main Display: Yes')) {
|
|
914
|
+
isMain = true;
|
|
915
|
+
} else if (trimmed.startsWith('Connection Type:')) {
|
|
916
|
+
connType = trimmed.replace('Connection Type:', '').trim();
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if (currentName && currentRes) {
|
|
920
|
+
displays.push({ name: currentName, resolution: currentRes, isMain, connectionType: connType || undefined });
|
|
921
|
+
}
|
|
922
|
+
return displays;
|
|
858
923
|
} catch {
|
|
859
924
|
return [];
|
|
860
925
|
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
export interface TimeMachineStatus {
|
|
929
|
+
configured: boolean;
|
|
930
|
+
running: boolean;
|
|
931
|
+
percent?: number;
|
|
932
|
+
latestBackup?: string;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
export async function getTimeMachineStatus(): Promise<TimeMachineStatus> {
|
|
936
|
+
try {
|
|
937
|
+
const statusRaw = await run('tmutil status 2>&1', '');
|
|
938
|
+
if (statusRaw.includes('No Time Machine destination') || statusRaw.includes('not configured')) {
|
|
939
|
+
return { configured: false, running: false };
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const running = statusRaw.includes('Running = 1;');
|
|
943
|
+
let percent: number | undefined = undefined;
|
|
944
|
+
const pctMatch = statusRaw.match(/Percent\s*=\s*"?(0\.\d+|\d+)"?/i);
|
|
945
|
+
if (pctMatch) {
|
|
946
|
+
percent = Math.round(parseFloat(pctMatch[1]) * 100);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
let latestBackup = 'Unknown';
|
|
950
|
+
try {
|
|
951
|
+
const latestRaw = await run('tmutil latestbackup 2>&1', '');
|
|
952
|
+
if (latestRaw.trim() && !latestRaw.includes('error') && !latestRaw.includes('No backups')) {
|
|
953
|
+
const basename = latestRaw.trim().split('/').pop() || latestRaw.trim();
|
|
954
|
+
latestBackup = basename.replace('.previous', '').replace('.backup', '');
|
|
955
|
+
} else {
|
|
956
|
+
latestBackup = 'No backup found / Unmounted';
|
|
957
|
+
}
|
|
958
|
+
} catch {
|
|
959
|
+
latestBackup = 'Not mounted';
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return { configured: true, running, percent, latestBackup };
|
|
963
|
+
} catch {
|
|
964
|
+
return { configured: false, running: false };
|
|
965
|
+
}
|
|
861
966
|
}
|
package/src/monitors/types.ts
CHANGED
|
@@ -80,6 +80,12 @@ export interface MemoryData {
|
|
|
80
80
|
swapFree: number;
|
|
81
81
|
pageSize: number;
|
|
82
82
|
usagePercent: number;
|
|
83
|
+
wiredBytes?: number;
|
|
84
|
+
compressedBytes?: number;
|
|
85
|
+
purgeableBytes?: number;
|
|
86
|
+
swapIns?: number;
|
|
87
|
+
swapOuts?: number;
|
|
88
|
+
pressureLevel?: string;
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
export interface ThermalData {
|