pyre-cli 9.5.1 → 10.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.
Files changed (94) hide show
  1. package/README.md +84 -14
  2. package/dist/alertChannels-TPSQIJ7S.js +8 -0
  3. package/dist/blender-FGH3ASEQ.js +7 -0
  4. package/dist/brewHealth-P2GW5Y5C.js +176 -0
  5. package/dist/buildCmd-CDPZ2ZFA.js +38 -0
  6. package/dist/{checkCmd-EWFNLBWE.js → checkCmd-RKW6QGDR.js} +10 -11
  7. package/dist/{chunk-RVGKEJXT.js → chunk-53O2RGSL.js} +47 -17
  8. package/dist/{chunk-MOAEGMFP.js → chunk-6UHZP4QP.js} +204 -46
  9. package/dist/chunk-GHRDW646.js +2506 -0
  10. package/dist/{prometheus-LFYHF6VO.js → chunk-HT4VQ4OH.js} +1 -0
  11. package/dist/chunk-M73HCQD2.js +95 -0
  12. package/dist/chunk-MZO5QKUR.js +32 -0
  13. package/dist/chunk-VIACN2RU.js +133 -0
  14. package/dist/chunk-Z2YV5HKV.js +87 -0
  15. package/dist/chunk-ZLVIVFSO.js +110 -0
  16. package/dist/{collectors-WD6VKH6M.js → collectors-H43PN4PS.js} +8 -2
  17. package/dist/{completions-VQMHQCXE.js → completions-JKYEJNGR.js} +34 -1
  18. package/dist/{config-4ZLF4HTS.js → config-GYGJXU2Q.js} +9 -1
  19. package/dist/doctor-GYHFTNZQ.js +387 -0
  20. package/dist/explainCmd-MF6PIMLW.js +222 -0
  21. package/dist/extensions-XJGHG7X3.js +134 -0
  22. package/dist/index.js +841 -206
  23. package/dist/ioreport.py +72 -0
  24. package/dist/netUsageCmd-WKBTW2YG.js +90 -0
  25. package/dist/{pipeCmd-7THXEGDH.js → pipeCmd-7REDBLBU.js} +5 -3
  26. package/dist/profileProcCmd-N4H6MCBT.js +107 -0
  27. package/dist/prometheus-5BL5PVLC.js +6 -0
  28. package/dist/prometheusCmd-PL64BLN5.js +93 -0
  29. package/dist/replayCmd-Z3PYXY7V.js +164 -0
  30. package/dist/run-HOGQDDTA.js +8 -0
  31. package/dist/serveCmd-2H5VLF3P.js +153 -0
  32. package/dist/service-Q74BBILO.js +181 -0
  33. package/dist/smartCmd-ODWKSFNG.js +166 -0
  34. package/dist/syncCmd-A2W3QB3S.js +126 -0
  35. package/dist/topoCmd-DU2HHZNH.js +148 -0
  36. package/dist/watchdogCmd-UF53JPZB.js +161 -0
  37. package/dist/{wizard-GVIAHIVV.js → wizard-TD7BGX6O.js} +1 -1
  38. package/install.ps1 +29 -0
  39. package/install.sh +10 -9
  40. package/package.json +7 -3
  41. package/src/ai/models.ts +138 -0
  42. package/src/alertChannels.ts +119 -0
  43. package/src/anomalies.ts +114 -60
  44. package/src/brewHealth.ts +157 -40
  45. package/src/buildCmd.ts +48 -0
  46. package/src/checkCmd.ts +5 -5
  47. package/src/completions.ts +35 -0
  48. package/src/doctor.ts +235 -7
  49. package/src/easy_features.test.ts +40 -0
  50. package/src/explainCmd.ts +261 -0
  51. package/src/extensions.ts +71 -7
  52. package/src/formatters/index.ts +1 -1
  53. package/src/formatters/output.ts +100 -13
  54. package/src/formatters/render.ts +274 -38
  55. package/src/formatters/themes.ts +8 -0
  56. package/src/formatters/types.ts +6 -1
  57. package/src/index.ts +298 -56
  58. package/src/live/input.ts +122 -4
  59. package/src/live/render.ts +98 -0
  60. package/src/live/state.ts +70 -52
  61. package/src/live/types.ts +2 -2
  62. package/src/monitors/batteryPredictor.ts +143 -0
  63. package/src/monitors/blender.ts +144 -0
  64. package/src/monitors/buildTracker.ts +121 -0
  65. package/src/monitors/collectors.ts +558 -147
  66. package/src/monitors/dns_cache.ts +48 -0
  67. package/src/monitors/index.ts +3 -2
  68. package/src/monitors/ioreport.py +72 -0
  69. package/src/monitors/platform/linux.ts +756 -0
  70. package/src/monitors/platform/windows.ts +519 -0
  71. package/src/monitors/run.ts +27 -3
  72. package/src/monitors/smc.ts +21 -1
  73. package/src/monitors/types.ts +112 -14
  74. package/src/netUsageCmd.ts +121 -0
  75. package/src/platform.test.ts +99 -0
  76. package/src/plugins/index.ts +143 -0
  77. package/src/profileProcCmd.ts +133 -0
  78. package/src/prometheusCmd.ts +107 -0
  79. package/src/replayCmd.ts +186 -0
  80. package/src/serveCmd.ts +180 -0
  81. package/src/service.ts +125 -7
  82. package/src/smartCmd.ts +205 -0
  83. package/src/state/config.ts +64 -17
  84. package/src/syncCmd.ts +166 -0
  85. package/src/topoCmd.ts +176 -0
  86. package/src/watchdogCmd.ts +198 -0
  87. package/src/xbar.ts +22 -11
  88. package/dist/brewHealth-F2BXF5M6.js +0 -74
  89. package/dist/chunk-FH5WDJBF.js +0 -15
  90. package/dist/chunk-HGRBBPYX.js +0 -937
  91. package/dist/doctor-L6FZYA7F.js +0 -184
  92. package/dist/extensions-DVA5YLGH.js +0 -74
  93. package/dist/service-WTS3NBQU.js +0 -73
  94. /package/dist/{chunk-SG26Y3WF.js → chunk-IM3IQMMT.js} +0 -0
@@ -24,6 +24,7 @@ export interface ThemeColors {
24
24
  disk: (s: string) => string;
25
25
  graphs: (s: string) => string;
26
26
  process: (s: string) => string;
27
+ blender: (s: string) => string;
27
28
  }
28
29
 
29
30
  export type { StatsData };
@@ -40,6 +41,8 @@ export interface VisibleItems {
40
41
  tasks?: boolean;
41
42
  disk?: boolean;
42
43
  process?: boolean;
44
+ containers?: boolean;
45
+ blender?: boolean;
43
46
  tree?: boolean;
44
47
  }
45
48
 
@@ -67,7 +70,7 @@ export interface TableOptions {
67
70
  /** Show process tree view instead of flat list. */
68
71
  treeView?: boolean;
69
72
  /** Active panel for detail/focus view. 'grid' means show all panels. */
70
- activePanel?: 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process' | 'p2p' | 'anomalies';
73
+ activePanel?: 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process' | 'p2p' | 'anomalies' | 'containers' | 'blender';
71
74
  /** Active anomaly alerts from the statistical detector. */
72
75
  anomalies?: AnomalyAlert[];
73
76
  /** Historical anomaly alerts for the anomalies tab. */
@@ -88,4 +91,6 @@ export interface TableOptions {
88
91
  inspectProcess?: any;
89
92
  /** Minimal mode: renders only CPU, GPU, Memory, and Temperature as large glanceable gauges. */
90
93
  minimal?: boolean;
94
+ /** Current selected AI diagnosis model ID. */
95
+ aiModel?: string;
91
96
  }
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * pyre - Mac system monitoring CLI
4
- * Monitors temps, cpu, memory, disk, battery, and system stats.
3
+ * pyre - Cross-platform system monitoring CLI
4
+ * Monitors temps, cpu, memory, disk, battery, and system stats on macOS, Linux, and Windows.
5
5
  * Live mode is a full interactive dashboard: graphs, pause/resume,
6
6
  * on-demand snapshot export, and continuous CSV logging.
7
7
  * P2P mode enables live data streaming between two systems over TCP.
@@ -18,7 +18,7 @@ import { startLive, stopLive } from './live/index.js';
18
18
  import { showSplash } from './splash.js';
19
19
  import { P2PServer, P2PClient } from './p2p/index.js';
20
20
  import { runDoctor, printDoctorReport } from './doctor.js';
21
- import { generateZshCompletions, generateBashCompletions, generateFishCompletions } from './completions.js';
21
+ import { generateZshCompletions, generateBashCompletions, generateFishCompletions, generatePowerShellCompletions } from './completions.js';
22
22
  import { runHistoryCommand } from './historyCmd.js';
23
23
  import { runDiffCommand } from './diffCmd.js';
24
24
  import { readConfig, CONFIG_FILE } from './state/config.js';
@@ -34,7 +34,7 @@ const program = new Command();
34
34
  program
35
35
  .name('pyre')
36
36
  .version(pkg.version)
37
- .description('Mac system monitoring CLI: interactive live dashboard, stats, graphs, export, packet monitor, battery predictor')
37
+ .description('Cross-platform system monitoring CLI: interactive live dashboard, stats, graphs, export, packet monitor, battery predictor')
38
38
  .option('-j, --json', 'Output as JSON')
39
39
  .option('--html', 'Output as HTML')
40
40
  .option('--md', 'Output as Markdown')
@@ -79,35 +79,64 @@ program
79
79
  .option('--no-wizard', 'Skip first-run interactive setup wizard')
80
80
  .option('--install', 'Install pyre web as a background launchd agent')
81
81
  .option('--uninstall', 'Uninstall pyre web launchd agent')
82
- .option('--port <port>', 'Port number for web server mode', '3000');
82
+ .option('--port <port>', 'Port number for web server mode', '3000')
83
+ .option('--prometheus-port <port>', 'Port number for Prometheus exporter (default: 9090)', '9090')
84
+ .option('--speed <n>', 'Playback speed multiplier for pyre replay (default: 1)', '1')
85
+ .option('--llm-backend <backend>', 'AI backend: builtin (local zero-setup), ollama, or openai (default: builtin)', 'builtin')
86
+ .option('--llm-model <model>', 'AI model name for pyre explain (default: expert-rules-v1 / llama3.2 / gpt-4o-mini)')
87
+ .option('--api-key <key>', 'API key for OpenAI-compatible LLM backend (or set OPENAI_API_KEY env)')
88
+ .option('--alert-slack <url>', 'Slack webhook URL for alert notifications')
89
+ .option('--alert-discord <url>', 'Discord webhook URL for alert notifications')
90
+ .option('--alert-pushover-token <token>', 'Pushover application token for alert notifications')
91
+ .option('--alert-pushover-user <user>', 'Pushover user key for alert notifications')
92
+ .option('--alert-ntfy <url>', 'ntfy.sh topic URL for alert notifications (e.g. https://ntfy.sh/my-topic)')
93
+ .option('--watchdog-config <file>', 'Path to custom watchdog JSON configuration rules')
94
+ .option('--dry-run', 'Simulate actions in watchdog mode without executing kills')
95
+ .option('--top <n>', 'Number of items to display in netusage command (default: 15)')
96
+ .option('--raw', 'Print unparsed output for profiler')
97
+ .option('--sync-dir <dir>', 'Custom directory path for cloud profile/config sync')
98
+ .option('--scan', 'Perform active network sweep on LAN neighbors for topology map');
83
99
 
84
100
  program.addHelpText('after', `
85
- Commands:
86
- live Interactive live dashboard
87
- check One-line plain-English health summary
88
- pipe Continuous newline-delimited JSON stream for scripting
89
- stress Synthetic CPU/GPU load generator
90
- ui Launch the native macOS UI dashboard
91
- web Serve an auto-refreshing live web portal
92
- ssh <host> Stream live stats from a remote Mac over SSH
93
- fleet <host1> [host2]... Multi-host live dashboard monitoring multiple Macs
94
- bench <cmd> Log CPU, memory, network, power draw, and estimate kWh cost
95
- benchmark Run a 1-minute CPU benchmark calculating digits of PI
96
- anomalies Compute z-score anomalies and print plain-language digest
97
- doctor Run system diagnostics
98
- extensions System Extensions inspector
99
- brew Homebrew health panel
100
- update Check for pyre-cli updates
101
- profile <save|load|list> Atomic configuration profile management
102
- config <show|reset> View or reset persistent configuration file
103
- history Graph historical resource trends from CSV logs
104
- diff <file1> <file2> Compare two saved snapshot files side-by-side
105
- info Concise hardware summary
106
- completions <shell> Generate shell auto-completion scripts
107
- xbar Generate an xbar / SwiftBar menu bar plugin script
108
- p2p <server|connect> Start a P2P server or connect to one
109
- server Print commands for starting P2P server/client
110
- `);
101
+ Commands:
102
+ live Interactive live dashboard
103
+ check One-line plain-English health summary
104
+ pipe Continuous newline-delimited JSON stream for scripting
105
+ stress Synthetic CPU/GPU load generator
106
+ ui Launch the live UI dashboard window
107
+ web Serve an auto-refreshing live web portal
108
+ ssh <host> Stream live stats from a remote host over SSH
109
+ fleet <host1> [host2]... Multi-host live dashboard monitoring multiple hosts
110
+ bench <cmd> Log CPU, memory, network, power draw, and estimate kWh cost
111
+ benchmark Run a 1-minute CPU benchmark calculating digits of PI
112
+ anomalies Compute z-score anomalies and print plain-language digest
113
+ doctor Run system diagnostics
114
+ extensions System Extensions / Driver inspector
115
+ brew Package manager health panel (brew/apt/pacman/winget)
116
+ update Check for pyre-cli updates
117
+ profile <save|load|list> Atomic configuration profile management
118
+ config <show|reset> View or reset persistent configuration file
119
+ history Graph historical resource trends from CSV logs
120
+ diff <file1> <file2> Compare two saved snapshot files side-by-side
121
+ info Concise hardware summary
122
+ completions <shell> Generate shell auto-completion scripts (zsh, bash, fish, powershell)
123
+ xbar Generate an xbar / SwiftBar menu bar plugin script
124
+ p2p <server|connect> Start a P2P server or connect to one
125
+ server Print commands for starting P2P server/client
126
+ blender Track active Blender render jobs and their progress
127
+ prometheus Start a Prometheus metrics exporter on localhost
128
+ smart S.M.A.R.T. disk health report (requires smartmontools)
129
+ replay <file> Play back a pyre CSV log file in the TUI
130
+ explain AI-powered plain-English anomaly explanation (Ollama/OpenAI)
131
+ alert test Send a test notification to all configured alert channels
132
+ netusage Real-time per-application network usage breakdown
133
+ serve Lightweight structured JSON REST API server
134
+ profile-proc <pid> CPU call-tree profiler and hot function sampler
135
+ watchdog Persistent autonomous monitoring daemon and rules engine
136
+ build Active compiler and build system tracker
137
+ topo Network topology mapper and pyre peer auto-discovery
138
+ sync <push|pull|status> Synchronize config profiles across Macs via iCloud/Drive
139
+ `);
111
140
 
112
141
  program.parse(process.argv);
113
142
 
@@ -233,6 +262,184 @@ async function main() {
233
262
  return;
234
263
  }
235
264
 
265
+ if (cmd === 'blender') {
266
+ const { collectBlenderRenders } = await import('./monitors/blender.js');
267
+ const renders = await collectBlenderRenders();
268
+ if (!renders.length) {
269
+ console.log(chalk.dim('No active Blender renders detected.'));
270
+ return;
271
+ }
272
+ const lines: string[] = [];
273
+ lines.push(chalk.bold(' Blender Render Tracker'));
274
+ lines.push('');
275
+ for (const r of renders) {
276
+ const statusColor = r.status === 'rendering' ? chalk.green : r.status === 'finalizing' ? chalk.yellow : r.status === 'loading' ? chalk.cyan : chalk.dim;
277
+ lines.push(` PID: ${r.pid}`);
278
+ lines.push(` Blend File: ${r.blendFile}`);
279
+ lines.push(` Engine: ${r.renderEngine}`);
280
+ lines.push(` Frames: ${r.currentFrame} / ${r.totalFrames} (${r.completionPercent}%)`);
281
+ lines.push(` Elapsed: ${r.elapsedSec}s`);
282
+ lines.push(` Status: ${statusColor(r.status)}`);
283
+ lines.push(` Output: ${r.outputPath}`);
284
+ if (r.sampleCount !== undefined) lines.push(` Samples: ${r.sampleCount}`);
285
+ lines.push('');
286
+ }
287
+ console.log(lines.join('\n'));
288
+ return;
289
+ }
290
+
291
+ if (cmd === 'prometheus') {
292
+ const { runPrometheusServer } = await import('./prometheusCmd.js');
293
+ const prometheusPort = parseInt(opts.prometheusPort, 10) || 9090;
294
+ const intervalSec = parseFloat(opts.interval) || 15;
295
+ await runPrometheusServer({ port: prometheusPort, interval: intervalSec, detailed: opts.detailed });
296
+ return;
297
+ }
298
+
299
+ if (cmd === 'smart') {
300
+ const { runSmartCommand } = await import('./smartCmd.js');
301
+ await runSmartCommand();
302
+ return;
303
+ }
304
+
305
+ if (cmd === 'replay') {
306
+ const file = program.args[1];
307
+ if (!file) {
308
+ console.error(chalk.red(' ✖ Usage: pyre replay <logfile.csv>'));
309
+ process.exit(1);
310
+ }
311
+ const { runReplayCommand } = await import('./replayCmd.js');
312
+ await runReplayCommand({
313
+ file,
314
+ speed: parseFloat(opts.speed) || 1,
315
+ plain: opts.plain || opts.a11y,
316
+ });
317
+ return;
318
+ }
319
+
320
+ if (cmd === 'explain') {
321
+ const { runExplainCommand } = await import('./explainCmd.js');
322
+ const backend = opts.llmBackend === 'openai' ? 'openai' : opts.llmBackend === 'ollama' ? 'ollama' : 'builtin';
323
+ await runExplainCommand({
324
+ since: opts.since || '7d',
325
+ backend,
326
+ model: opts.llmModel,
327
+ apiKey: opts.apiKey,
328
+ dir: opts.exportDir,
329
+ });
330
+ return;
331
+ }
332
+
333
+ if (cmd === 'alert') {
334
+ const sub = program.args[1];
335
+ if (sub === 'test') {
336
+ const { sendTestAlert } = await import('./alertChannels.js');
337
+ const { writeConfig } = await import('./state/config.js');
338
+ // Persist any channel flags passed on CLI before sending test
339
+ if (opts.alertSlack || opts.alertDiscord || opts.alertPushoverToken || opts.alertPushoverUser || opts.alertNtfy) {
340
+ writeConfig({
341
+ slackAlertUrl: opts.alertSlack || config.slackAlertUrl,
342
+ discordAlertUrl: opts.alertDiscord || config.discordAlertUrl,
343
+ pushoverToken: opts.alertPushoverToken || config.pushoverToken,
344
+ pushoverUser: opts.alertPushoverUser || config.pushoverUser,
345
+ ntfyUrl: opts.alertNtfy || config.ntfyUrl,
346
+ });
347
+ }
348
+ const testConfig = {
349
+ slackUrl: opts.alertSlack || config.slackAlertUrl || undefined,
350
+ discordUrl: opts.alertDiscord || config.discordAlertUrl || undefined,
351
+ pushoverToken: opts.alertPushoverToken || config.pushoverToken || undefined,
352
+ pushoverUser: opts.alertPushoverUser || config.pushoverUser || undefined,
353
+ ntfyUrl: opts.alertNtfy || config.ntfyUrl || undefined,
354
+ };
355
+ const hasAny = Object.values(testConfig).some(v => !!v);
356
+ if (!hasAny) {
357
+ console.log(chalk.yellow('\n ⚠ No alert channels configured.'));
358
+ console.log(chalk.dim(' Use --alert-slack <url>, --alert-discord <url>, --alert-ntfy <url>, etc.\n'));
359
+ return;
360
+ }
361
+ console.log(chalk.bold('\n 🔥 pyre alert test — firing test notification...\n'));
362
+ await sendTestAlert(testConfig);
363
+ console.log(chalk.green(' ✔ Test alert sent to all configured channels.\n'));
364
+ } else {
365
+ console.log(chalk.yellow(' Usage: pyre alert test'));
366
+ }
367
+ return;
368
+ }
369
+
370
+ if (cmd === 'netusage') {
371
+ const { runNetUsageCommand } = await import('./netUsageCmd.js');
372
+ await runNetUsageCommand({
373
+ top: opts.top ? parseInt(opts.top, 10) : 15,
374
+ sort: (opts.sort === 'rx' || opts.sort === 'tx') ? opts.sort : 'total',
375
+ json: !!opts.json,
376
+ });
377
+ return;
378
+ }
379
+
380
+ if (cmd === 'serve') {
381
+ const { runServeCommand } = await import('./serveCmd.js');
382
+ const port = parseInt(opts.port, 10) || 8080;
383
+ await runServeCommand({
384
+ port,
385
+ apiKey: opts.apiKey,
386
+ detailed: opts.detailed,
387
+ });
388
+ return;
389
+ }
390
+
391
+ if (cmd === 'profile-proc') {
392
+ const targetPid = parseInt(program.args[1], 10);
393
+ if (isNaN(targetPid)) {
394
+ console.error(chalk.red(' ✖ Usage: pyre profile-proc <pid> [--duration <sec>]'));
395
+ process.exit(1);
396
+ }
397
+ const { runProfileProcCommand } = await import('./profileProcCmd.js');
398
+ const durationSec = parseFloat(opts.duration) || 5;
399
+ await runProfileProcCommand({
400
+ pid: targetPid,
401
+ durationSec,
402
+ raw: !!opts.raw,
403
+ });
404
+ return;
405
+ }
406
+
407
+ if (cmd === 'watchdog') {
408
+ const { runWatchdogCommand } = await import('./watchdogCmd.js');
409
+ await runWatchdogCommand({
410
+ configFile: opts.watchdogConfig,
411
+ dryRun: !!opts.dryRun,
412
+ interval: parseFloat(opts.interval) || 2,
413
+ });
414
+ return;
415
+ }
416
+
417
+ if (cmd === 'build') {
418
+ const { runBuildCommand } = await import('./buildCmd.js');
419
+ await runBuildCommand({
420
+ json: !!opts.json,
421
+ });
422
+ return;
423
+ }
424
+
425
+ if (cmd === 'topo') {
426
+ const { runTopoCommand } = await import('./topoCmd.js');
427
+ await runTopoCommand({
428
+ scan: !!opts.scan,
429
+ json: !!opts.json,
430
+ });
431
+ return;
432
+ }
433
+
434
+ if (cmd === 'sync') {
435
+ const subcommand = program.args[1] || 'status';
436
+ const { runSyncCommand } = await import('./syncCmd.js');
437
+ await runSyncCommand(subcommand, {
438
+ syncDir: opts.syncDir,
439
+ });
440
+ return;
441
+ }
442
+
236
443
  if (cmd === 'brew') {
237
444
  const { printBrewHealthReport } = await import('./brewHealth.js');
238
445
  await printBrewHealthReport();
@@ -399,6 +606,12 @@ async function main() {
399
606
  if (opts.webhookUrl) config.webhookUrl = opts.webhookUrl;
400
607
  if (opts.alertCmd) config.alertCmd = opts.alertCmd;
401
608
  if (opts.exportDir) config.exportDir = opts.exportDir;
609
+ // Persist new alert channel options to config if provided
610
+ if (opts.alertSlack) config.slackAlertUrl = opts.alertSlack;
611
+ if (opts.alertDiscord) config.discordAlertUrl = opts.alertDiscord;
612
+ if (opts.alertPushoverToken) config.pushoverToken = opts.alertPushoverToken;
613
+ if (opts.alertPushoverUser) config.pushoverUser = opts.alertPushoverUser;
614
+ if (opts.alertNtfy) config.ntfyUrl = opts.alertNtfy;
402
615
  const tempUnit = opts.tempUnit === 'f' ? 'f' : 'c';
403
616
 
404
617
  if (cmd === 'live' || (!isExportMode() && !opts.once && cmd !== 'p2p')) {
@@ -669,7 +882,7 @@ async function runInfoCommand(): Promise<void> {
669
882
  const tmStatus = await getTimeMachineStatus();
670
883
 
671
884
  const lines: string[] = [];
672
- lines.push(chalk.bold(' pyre info — Mac System Information'));
885
+ lines.push(chalk.bold(` pyre info — System Information (${data.header.os})`));
673
886
  lines.push('');
674
887
  lines.push(` Hostname: ${data.header.hostname}`);
675
888
  lines.push(` OS: ${data.header.os}`);
@@ -696,13 +909,15 @@ async function runInfoCommand(): Promise<void> {
696
909
  }
697
910
  }
698
911
 
699
- lines.push('');
700
- lines.push(chalk.cyan.bold(' ⏱ Time Machine Backup:'));
701
- if (!tmStatus.configured) {
702
- lines.push(' • Not configured');
703
- } else {
704
- lines.push(` • Status: ${tmStatus.running ? chalk.yellow(`Backup in progress (${tmStatus.percent || 0}%)`) : chalk.green('Idle')}`);
705
- lines.push(` • Latest Backup: ${tmStatus.latestBackup}`);
912
+ if (process.platform === 'darwin') {
913
+ lines.push('');
914
+ lines.push(chalk.cyan.bold(' ⏱ Time Machine Backup:'));
915
+ if (!tmStatus.configured) {
916
+ lines.push(' • Not configured');
917
+ } else {
918
+ lines.push(` • Status: ${tmStatus.running ? chalk.yellow(`Backup in progress (${tmStatus.percent || 0}%)`) : chalk.green('Idle')}`);
919
+ lines.push(` • Latest Backup: ${tmStatus.latestBackup}`);
920
+ }
706
921
  }
707
922
 
708
923
  console.log(lines.join('\n'));
@@ -850,12 +1065,29 @@ async function runWebCommand(): Promise<number> {
850
1065
  });
851
1066
  }
852
1067
 
1068
+ async function openBrowserUrl(url: string): Promise<void> {
1069
+ const { spawn } = await import('node:child_process');
1070
+ const platform = process.platform;
1071
+ if (platform === 'win32') {
1072
+ spawn('cmd.exe', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' });
1073
+ } else if (platform === 'darwin') {
1074
+ spawn('open', [url], { detached: true, stdio: 'ignore' });
1075
+ } else {
1076
+ spawn('xdg-open', [url], { detached: true, stdio: 'ignore' });
1077
+ }
1078
+ }
1079
+
853
1080
  async function runUiCommand(): Promise<void> {
854
1081
  // Use a random ephemeral port to avoid collisions
855
1082
  opts.port = '0';
856
1083
  const port = await runWebCommand();
857
-
858
- const swiftScript = `
1084
+ const url = `http://localhost:${port}`;
1085
+
1086
+ if (process.platform === 'darwin') {
1087
+ const { run } = await import('./monitors/run.js');
1088
+ const hasSwift = (await run('which swift 2>/dev/null', '')).trim().length > 0;
1089
+ if (hasSwift) {
1090
+ const swiftScript = `
859
1091
  import Cocoa
860
1092
  import WebKit
861
1093
 
@@ -958,23 +1190,31 @@ app.delegate = delegate
958
1190
  app.run()
959
1191
  `;
960
1192
 
1193
+ const scriptPath = path.join(os.tmpdir(), 'pyre-ui.swift');
1194
+ fs.writeFileSync(scriptPath, swiftScript);
961
1195
 
962
- const scriptPath = path.join(os.tmpdir(), 'pyre-ui.swift');
963
- fs.writeFileSync(scriptPath, swiftScript);
1196
+ const { spawn } = await import('node:child_process');
1197
+ console.log(chalk.cyan(`\n Launching native UI window...`));
1198
+ const ui = spawn('swift', [scriptPath], { stdio: 'inherit' });
1199
+
1200
+ ui.on('close', () => {
1201
+ console.log(chalk.dim('UI window closed, shutting down server...'));
1202
+ process.exit(0);
1203
+ });
964
1204
 
965
- const { spawn } = await import('node:child_process');
966
- console.log(chalk.cyan(`\n Launching native UI window...`));
967
- const ui = spawn('swift', [scriptPath], { stdio: 'inherit' });
968
-
969
- ui.on('close', () => {
970
- console.log(chalk.dim('UI window closed, shutting down server...'));
971
- process.exit(0);
972
- });
1205
+ process.once('SIGINT', () => {
1206
+ ui.kill();
1207
+ process.exit(0);
1208
+ });
1209
+ return;
1210
+ }
1211
+ }
973
1212
 
974
- process.once('SIGINT', () => {
975
- ui.kill();
976
- process.exit(0);
977
- });
1213
+ // Cross-platform browser / app-mode launch for Linux, Windows, or macOS without Swift
1214
+ console.log(chalk.cyan(`\n Opening live UI dashboard at ${url}...`));
1215
+ await openBrowserUrl(url);
1216
+ console.log(chalk.dim(' Press Ctrl+C to stop server.\n'));
1217
+ await new Promise(() => {});
978
1218
  }
979
1219
 
980
1220
  async function runBenchCommand(benchCmd: string): Promise<void> {
@@ -994,7 +1234,9 @@ async function runBenchCommand(benchCmd: string): Promise<void> {
994
1234
  let accumulatedWattSeconds = 0;
995
1235
  let powerSamplesCount = 0;
996
1236
 
997
- const child = spawn('sh', ['-c', benchCmd]);
1237
+ const child = process.platform === 'win32'
1238
+ ? spawn('cmd.exe', ['/c', benchCmd], { stdio: 'inherit' })
1239
+ : spawn('sh', ['-c', benchCmd]);
998
1240
  child.stdout.on('data', d => process.stdout.write(d));
999
1241
  child.stderr.on('data', d => process.stdout.write(d));
1000
1242
 
package/src/live/input.ts CHANGED
@@ -16,6 +16,8 @@ import { render, footerLine, checkAlerts, invalidateTableCache, invalidateFrame
16
16
  import type { LiveOptions, ExportFormat, InputMode, SortMode, GraphMode, ActivePanel } from './types.js';
17
17
  import { startP2PServer } from '../p2p/index.js';
18
18
  import { writeConfig } from '../state/config.js';
19
+ import { AVAILABLE_AI_MODELS } from '../ai/models.js';
20
+ import { loadPlugins, executePlugins } from '../plugins/index.js';
19
21
 
20
22
  function persistConfig() {
21
23
  writeConfig({
@@ -38,6 +40,9 @@ import { writeConfig } from '../state/config.js';
38
40
  splashAnimation: state.splashAnimation,
39
41
  visiblePanels: { ...state.visiblePanels },
40
42
  notificationsEnabled: state.notificationsEnabled,
43
+ dockerModeConfirmed: state.dockerModeConfirmed,
44
+ aiModel: state.aiModel,
45
+ aiBackend: state.aiBackend,
41
46
  });
42
47
  }
43
48
 
@@ -95,6 +100,25 @@ function handleInputModeKey(str: string, key: readline.Key) {
95
100
  } else if (selected === 'Temperature Unit') {
96
101
  state.tempUnit = state.tempUnit === 'c' ? 'f' : 'c';
97
102
  setStatus(`Temperature unit: ${state.tempUnit.toUpperCase()}`);
103
+ } else if (selected === 'AI Model') {
104
+ const curIdx = AVAILABLE_AI_MODELS.findIndex(m => m.id === state.aiModel);
105
+ const nextIdx = (curIdx + 1) % AVAILABLE_AI_MODELS.length;
106
+ state.aiModel = AVAILABLE_AI_MODELS[nextIdx].id;
107
+ state.aiBackend = AVAILABLE_AI_MODELS[nextIdx].backend;
108
+ setStatus(`AI Model: ${AVAILABLE_AI_MODELS[nextIdx].name}`);
109
+ } else if (selected === 'Grid Panel Order') {
110
+ // Rotate panelLayout
111
+ const layouts = [
112
+ ['mem', 'disk', 'net'],
113
+ ['disk', 'mem', 'net'],
114
+ ['net', 'mem', 'disk'],
115
+ ['mem', 'net', 'disk'],
116
+ ];
117
+ const currentStr = state.panelLayout.join(',');
118
+ const currentIdx = layouts.findIndex(l => l.join(',') === currentStr);
119
+ const nextIdx = (currentIdx + 1) % layouts.length;
120
+ state.panelLayout = layouts[nextIdx];
121
+ setStatus(`Panel layout: ${state.panelLayout.join(' → ')}`);
98
122
  } else {
99
123
  const toggleKey = getToggleKey(selected);
100
124
  if (toggleKey) {
@@ -179,6 +203,82 @@ function handleInputModeKey(str: string, key: readline.Key) {
179
203
  return;
180
204
  }
181
205
 
206
+ if (state.inputMode === 'docker-alert') {
207
+ if (isEscKey(key, str)) {
208
+ state.inputMode = null;
209
+ state.dockerModeConfirmed = true;
210
+ render();
211
+ return;
212
+ }
213
+ if (isUpKey(key, str)) {
214
+ state.dockerSelectionIndex = Math.max(0, state.dockerSelectionIndex - 1);
215
+ render();
216
+ return;
217
+ }
218
+ if (isDownKey(key, str)) {
219
+ state.dockerSelectionIndex = Math.min(1, state.dockerSelectionIndex + 1);
220
+ render();
221
+ return;
222
+ }
223
+ if (isEnterKey(key, str) || str === ' ') {
224
+ state.dockerModeConfirmed = true;
225
+ state.inputMode = null;
226
+
227
+ if (state.dockerSelectionIndex === 0) {
228
+ // Docker Mode
229
+ state.visiblePanels.battery = false;
230
+ state.visiblePanels.thermal = false;
231
+ state.visiblePanels.power = false;
232
+ state.visiblePanels.gpu = false;
233
+ state.detailed = false;
234
+ } else {
235
+ // Regular TUI
236
+ state.visiblePanels.battery = true;
237
+ state.visiblePanels.thermal = true;
238
+ state.visiblePanels.power = true;
239
+ state.visiblePanels.gpu = true;
240
+ }
241
+
242
+ invalidateTableCache();
243
+ invalidateFrame();
244
+ persistConfig();
245
+ render();
246
+ return;
247
+ }
248
+ return;
249
+ }
250
+
251
+ if (state.inputMode === 'ai-model') {
252
+ if (isEscKey(key, str)) {
253
+ state.inputMode = null;
254
+ render();
255
+ return;
256
+ }
257
+ if (isUpKey(key, str) || str === 'k') {
258
+ state.modelSelectionIndex = (state.modelSelectionIndex - 1 + AVAILABLE_AI_MODELS.length) % AVAILABLE_AI_MODELS.length;
259
+ render();
260
+ return;
261
+ }
262
+ if (isDownKey(key, str) || str === 'j') {
263
+ state.modelSelectionIndex = (state.modelSelectionIndex + 1) % AVAILABLE_AI_MODELS.length;
264
+ render();
265
+ return;
266
+ }
267
+ if (isEnterKey(key, str) || str === ' ') {
268
+ const chosen = AVAILABLE_AI_MODELS[state.modelSelectionIndex];
269
+ state.aiModel = chosen.id;
270
+ state.aiBackend = chosen.backend;
271
+ state.inputMode = null;
272
+ setStatus(`Active AI Model: ${chosen.name}`, 4000);
273
+ invalidateTableCache();
274
+ invalidateFrame();
275
+ persistConfig();
276
+ render();
277
+ return;
278
+ }
279
+ return;
280
+ }
281
+
182
282
  if (state.inputMode === 'signal') {
183
283
  if (isUpKey(key, str)) {
184
284
  const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer as typeof state.SIGNAL_OPTIONS[number]);
@@ -442,6 +542,7 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
442
542
  state.history.reset();
443
543
  state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
444
544
 
545
+ await loadPlugins().catch(() => {});
445
546
  const warmupPromise = doWarmup();
446
547
 
447
548
  if (splashPromise) {
@@ -540,6 +641,18 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
540
641
  render();
541
642
  return;
542
643
  }
644
+ case 'C': {
645
+ state.activePanel = state.activePanel === 'containers' ? 'grid' : 'containers';
646
+ setStatus(state.activePanel === 'grid' ? 'Grid view' : 'Containers panel');
647
+ render();
648
+ return;
649
+ }
650
+ case 'B': {
651
+ state.activePanel = state.activePanel === 'blender' ? 'grid' : 'blender';
652
+ setStatus(state.activePanel === 'grid' ? 'Grid view' : 'Blender panel');
653
+ render();
654
+ return;
655
+ }
543
656
  }
544
657
 
545
658
  switch (key.name) {
@@ -568,6 +681,11 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
568
681
  state.customizerIndex = 0;
569
682
  render();
570
683
  break;
684
+ case 'm':
685
+ state.inputMode = 'ai-model';
686
+ state.modelSelectionIndex = Math.max(0, AVAILABLE_AI_MODELS.findIndex(m => m.id === state.aiModel));
687
+ render();
688
+ break;
571
689
  case 'p':
572
690
  state.paused = !state.paused;
573
691
  setStatus(state.paused ? 'Paused' : 'Resumed');
@@ -716,16 +834,16 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
716
834
  process.once('SIGINT', () => stopLive());
717
835
  }
718
836
 
719
- function tabKeyToId(str: string): string | null {
720
- const map: Record<string, string> = { '1': 'cpu', '2': 'mem', '3': 'gpu', '4': 'power', '5': 'battery', '6': 'thermal', '7': 'network', '8': 'packets', '9': 'tasks', '0': 'disk', 'p': 'process', 'r': 'p2p', 'A': 'anomalies' };
837
+ function tabKeyToId(str: string): ActivePanel | null {
838
+ const map: Record<string, ActivePanel> = { '1': 'cpu', '2': 'mem', '3': 'gpu', '4': 'power', '5': 'battery', '6': 'thermal', '7': 'network', '8': 'packets', '9': 'tasks', '0': 'disk', 'p': 'process', 'C': 'containers', 'r': 'p2p', 'A': 'anomalies', 'B': 'blender' };
721
839
  return map[str] ?? null;
722
840
  }
723
841
 
724
842
  function cycleTab(direction: number) {
725
- const tabs = state.PANEL_TABS.map(t => t.id);
843
+ const tabs = state.PANEL_TABS.map(t => t.id) as string[];
726
844
  const current = state.activePanel === 'grid' ? -1 : tabs.indexOf(state.activePanel);
727
845
  const next = (current + direction + tabs.length) % tabs.length;
728
- state.activePanel = tabs[next];
846
+ state.activePanel = tabs[next] as typeof state.activePanel;
729
847
  setStatus(`Panel: ${state.activePanel.toUpperCase()}`);
730
848
  render();
731
849
  }