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.
- package/README.md +84 -14
- package/dist/alertChannels-TPSQIJ7S.js +8 -0
- package/dist/blender-FGH3ASEQ.js +7 -0
- package/dist/brewHealth-P2GW5Y5C.js +176 -0
- package/dist/buildCmd-CDPZ2ZFA.js +38 -0
- package/dist/{checkCmd-EWFNLBWE.js → checkCmd-RKW6QGDR.js} +10 -11
- package/dist/{chunk-RVGKEJXT.js → chunk-53O2RGSL.js} +47 -17
- package/dist/{chunk-MOAEGMFP.js → chunk-6UHZP4QP.js} +204 -46
- package/dist/chunk-GHRDW646.js +2506 -0
- package/dist/{prometheus-LFYHF6VO.js → chunk-HT4VQ4OH.js} +1 -0
- package/dist/chunk-M73HCQD2.js +95 -0
- package/dist/chunk-MZO5QKUR.js +32 -0
- package/dist/chunk-VIACN2RU.js +133 -0
- package/dist/chunk-Z2YV5HKV.js +87 -0
- package/dist/chunk-ZLVIVFSO.js +110 -0
- package/dist/{collectors-WD6VKH6M.js → collectors-H43PN4PS.js} +8 -2
- package/dist/{completions-VQMHQCXE.js → completions-JKYEJNGR.js} +34 -1
- package/dist/{config-4ZLF4HTS.js → config-GYGJXU2Q.js} +9 -1
- package/dist/doctor-GYHFTNZQ.js +387 -0
- package/dist/explainCmd-MF6PIMLW.js +222 -0
- package/dist/extensions-XJGHG7X3.js +134 -0
- package/dist/index.js +841 -206
- package/dist/ioreport.py +72 -0
- package/dist/netUsageCmd-WKBTW2YG.js +90 -0
- package/dist/{pipeCmd-7THXEGDH.js → pipeCmd-7REDBLBU.js} +5 -3
- package/dist/profileProcCmd-N4H6MCBT.js +107 -0
- package/dist/prometheus-5BL5PVLC.js +6 -0
- package/dist/prometheusCmd-PL64BLN5.js +93 -0
- package/dist/replayCmd-Z3PYXY7V.js +164 -0
- package/dist/run-HOGQDDTA.js +8 -0
- package/dist/serveCmd-2H5VLF3P.js +153 -0
- package/dist/service-Q74BBILO.js +181 -0
- package/dist/smartCmd-ODWKSFNG.js +166 -0
- package/dist/syncCmd-A2W3QB3S.js +126 -0
- package/dist/topoCmd-DU2HHZNH.js +148 -0
- package/dist/watchdogCmd-UF53JPZB.js +161 -0
- package/dist/{wizard-GVIAHIVV.js → wizard-TD7BGX6O.js} +1 -1
- package/install.ps1 +29 -0
- package/install.sh +10 -9
- package/package.json +7 -3
- package/src/ai/models.ts +138 -0
- package/src/alertChannels.ts +119 -0
- package/src/anomalies.ts +114 -60
- package/src/brewHealth.ts +157 -40
- package/src/buildCmd.ts +48 -0
- package/src/checkCmd.ts +5 -5
- package/src/completions.ts +35 -0
- package/src/doctor.ts +235 -7
- package/src/easy_features.test.ts +40 -0
- package/src/explainCmd.ts +261 -0
- package/src/extensions.ts +71 -7
- package/src/formatters/index.ts +1 -1
- package/src/formatters/output.ts +100 -13
- package/src/formatters/render.ts +274 -38
- package/src/formatters/themes.ts +8 -0
- package/src/formatters/types.ts +6 -1
- package/src/index.ts +298 -56
- package/src/live/input.ts +122 -4
- package/src/live/render.ts +98 -0
- package/src/live/state.ts +70 -52
- package/src/live/types.ts +2 -2
- package/src/monitors/batteryPredictor.ts +143 -0
- package/src/monitors/blender.ts +144 -0
- package/src/monitors/buildTracker.ts +121 -0
- package/src/monitors/collectors.ts +558 -147
- package/src/monitors/dns_cache.ts +48 -0
- package/src/monitors/index.ts +3 -2
- package/src/monitors/ioreport.py +72 -0
- package/src/monitors/platform/linux.ts +756 -0
- package/src/monitors/platform/windows.ts +519 -0
- package/src/monitors/run.ts +27 -3
- package/src/monitors/smc.ts +21 -1
- package/src/monitors/types.ts +112 -14
- package/src/netUsageCmd.ts +121 -0
- package/src/platform.test.ts +99 -0
- package/src/plugins/index.ts +143 -0
- package/src/profileProcCmd.ts +133 -0
- package/src/prometheusCmd.ts +107 -0
- package/src/replayCmd.ts +186 -0
- package/src/serveCmd.ts +180 -0
- package/src/service.ts +125 -7
- package/src/smartCmd.ts +205 -0
- package/src/state/config.ts +64 -17
- package/src/syncCmd.ts +166 -0
- package/src/topoCmd.ts +176 -0
- package/src/watchdogCmd.ts +198 -0
- package/src/xbar.ts +22 -11
- package/dist/brewHealth-F2BXF5M6.js +0 -74
- package/dist/chunk-FH5WDJBF.js +0 -15
- package/dist/chunk-HGRBBPYX.js +0 -937
- package/dist/doctor-L6FZYA7F.js +0 -184
- package/dist/extensions-DVA5YLGH.js +0 -74
- package/dist/service-WTS3NBQU.js +0 -73
- /package/dist/{chunk-SG26Y3WF.js → chunk-IM3IQMMT.js} +0 -0
package/src/formatters/types.ts
CHANGED
|
@@ -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 -
|
|
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('
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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(
|
|
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
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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
|
-
|
|
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
|
-
|
|
963
|
-
|
|
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
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
});
|
|
1205
|
+
process.once('SIGINT', () => {
|
|
1206
|
+
ui.kill();
|
|
1207
|
+
process.exit(0);
|
|
1208
|
+
});
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
973
1212
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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 =
|
|
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):
|
|
720
|
-
const map: Record<string,
|
|
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
|
}
|