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/topoCmd.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pyre topo — Network Topology Mapper.
|
|
3
|
+
*
|
|
4
|
+
* Visualizes the local network topology, including default gateways,
|
|
5
|
+
* active routing paths, LAN neighbors (via ARP cache), and active remote sockets.
|
|
6
|
+
* Also scans LAN neighbors for running pyre instances (P2P port 9876, REST port 8080/3000).
|
|
7
|
+
*/
|
|
8
|
+
import net from 'node:net';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { run } from './monitors/run.js';
|
|
12
|
+
import { collectNetwork } from './monitors/collectors.js';
|
|
13
|
+
|
|
14
|
+
export interface TopoOptions {
|
|
15
|
+
scan?: boolean;
|
|
16
|
+
json?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface LanPeer {
|
|
20
|
+
ip: string;
|
|
21
|
+
mac: string;
|
|
22
|
+
interface: string;
|
|
23
|
+
isGateway: boolean;
|
|
24
|
+
pyreP2P?: boolean;
|
|
25
|
+
pyreRest?: boolean;
|
|
26
|
+
hostname?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function probePort(ip: string, port: number, timeoutMs = 400): Promise<boolean> {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const socket = new net.Socket();
|
|
32
|
+
socket.setTimeout(timeoutMs);
|
|
33
|
+
socket.once('connect', () => {
|
|
34
|
+
socket.destroy();
|
|
35
|
+
resolve(true);
|
|
36
|
+
});
|
|
37
|
+
socket.once('timeout', () => {
|
|
38
|
+
socket.destroy();
|
|
39
|
+
resolve(false);
|
|
40
|
+
});
|
|
41
|
+
socket.once('error', () => {
|
|
42
|
+
socket.destroy();
|
|
43
|
+
resolve(false);
|
|
44
|
+
});
|
|
45
|
+
socket.connect(port, ip);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function getDefaultGateway(): Promise<string> {
|
|
50
|
+
try {
|
|
51
|
+
const raw = await run("route -n get default 2>/dev/null | grep gateway | awk '{print $2}'", '');
|
|
52
|
+
if (raw.trim()) return raw.trim();
|
|
53
|
+
} catch {}
|
|
54
|
+
try {
|
|
55
|
+
const raw = await run("netstat -nr | grep default | awk '{print $2}' | head -n1", '');
|
|
56
|
+
if (raw.trim()) return raw.trim();
|
|
57
|
+
} catch {}
|
|
58
|
+
return '192.168.1.1';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function getArpNeighbors(gatewayIp: string): Promise<LanPeer[]> {
|
|
62
|
+
const peers: LanPeer[] = [];
|
|
63
|
+
try {
|
|
64
|
+
const raw = await run('arp -a -n 2>/dev/null', '');
|
|
65
|
+
const lines = raw.split('\n');
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
// macOS format: "? (192.168.1.1) at 0:11:22:33:44:55 on en0 ifscope [ethernet]"
|
|
68
|
+
const match = line.match(/\(([\d.]+)\)\s+at\s+([0-9a-fA-F:]+)\s+on\s+(\S+)/);
|
|
69
|
+
if (match) {
|
|
70
|
+
const ip = match[1];
|
|
71
|
+
const mac = match[2];
|
|
72
|
+
const iface = match[3];
|
|
73
|
+
if (ip === '255.255.255.255' || ip.startsWith('224.') || mac.includes('incomplete')) continue;
|
|
74
|
+
peers.push({
|
|
75
|
+
ip,
|
|
76
|
+
mac,
|
|
77
|
+
interface: iface,
|
|
78
|
+
isGateway: ip === gatewayIp,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch {}
|
|
83
|
+
return peers;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function runTopoCommand(opts: TopoOptions = {}): Promise<void> {
|
|
87
|
+
const localNet = await collectNetwork();
|
|
88
|
+
const gatewayIp = await getDefaultGateway();
|
|
89
|
+
const lanPeers = await getArpNeighbors(gatewayIp);
|
|
90
|
+
const hostname = os.hostname();
|
|
91
|
+
|
|
92
|
+
// If scan is requested or small peer count, probe common pyre ports
|
|
93
|
+
if (opts.scan || lanPeers.length <= 15) {
|
|
94
|
+
await Promise.all(
|
|
95
|
+
lanPeers.map(async (peer) => {
|
|
96
|
+
const [hasP2P, hasRest] = await Promise.all([
|
|
97
|
+
probePort(peer.ip, 9876, 250),
|
|
98
|
+
probePort(peer.ip, 8080, 250),
|
|
99
|
+
]);
|
|
100
|
+
peer.pyreP2P = hasP2P;
|
|
101
|
+
peer.pyreRest = hasRest;
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const establishedConns = (localNet.topRemoteHosts || []).slice(0, 6);
|
|
107
|
+
|
|
108
|
+
if (opts.json) {
|
|
109
|
+
console.log(
|
|
110
|
+
JSON.stringify(
|
|
111
|
+
{
|
|
112
|
+
localNode: {
|
|
113
|
+
hostname,
|
|
114
|
+
ip: localNet.ip,
|
|
115
|
+
interface: localNet.interface,
|
|
116
|
+
},
|
|
117
|
+
gateway: gatewayIp,
|
|
118
|
+
lanPeers,
|
|
119
|
+
activeRemoteConnections: establishedConns,
|
|
120
|
+
},
|
|
121
|
+
null,
|
|
122
|
+
2
|
|
123
|
+
)
|
|
124
|
+
);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log(chalk.bold('\n 🔥 pyre topo — Network Topology Map\n'));
|
|
129
|
+
|
|
130
|
+
// Local Host Header Box
|
|
131
|
+
console.log(` ${chalk.bgCyan.black.bold(' LOCAL HOST ')} ${chalk.bold(hostname)}`);
|
|
132
|
+
console.log(` ├── IP Address: ${chalk.green.bold(localNet.ip || '127.0.0.1')} (${localNet.interface})`);
|
|
133
|
+
console.log(` ├── Active RX / TX: ↓ ${chalk.cyan(localNet.rxBytes ? `${(localNet.rxBytes / 1e6).toFixed(1)} MB` : '0 MB')} ↑ ${chalk.blue(localNet.txBytes ? `${(localNet.txBytes / 1e6).toFixed(1)} MB` : '0 MB')}`);
|
|
134
|
+
console.log(' │');
|
|
135
|
+
|
|
136
|
+
// Gateway Node
|
|
137
|
+
console.log(` ├── ${chalk.yellow.bold('DEFAULT GATEWAY / ROUTER')}`);
|
|
138
|
+
console.log(` │ └── IP: ${chalk.yellow(gatewayIp)} [Route → WAN / Internet]`);
|
|
139
|
+
console.log(' │');
|
|
140
|
+
|
|
141
|
+
// LAN Neighborhood
|
|
142
|
+
console.log(` ├── ${chalk.magenta.bold('LAN NEIGHBORS & PEERS')} (${lanPeers.length} discovered via ARP)`);
|
|
143
|
+
if (lanPeers.length === 0) {
|
|
144
|
+
console.log(` │ └── ${chalk.dim('No neighboring ARP entries currently cached.')}`);
|
|
145
|
+
} else {
|
|
146
|
+
for (let i = 0; i < lanPeers.length; i++) {
|
|
147
|
+
const peer = lanPeers[i];
|
|
148
|
+
const isLast = i === lanPeers.length - 1;
|
|
149
|
+
const prefix = isLast ? ' │ └──' : ' │ ├──';
|
|
150
|
+
const gwBadge = peer.isGateway ? chalk.yellow(' [Gateway]') : '';
|
|
151
|
+
let pyreBadge = '';
|
|
152
|
+
if (peer.pyreP2P) pyreBadge += chalk.hex('#ff5500').bold(' [🔥 pyre P2P :9876]');
|
|
153
|
+
if (peer.pyreRest) pyreBadge += chalk.hex('#00ffcc').bold(' [🔥 pyre REST :8080]');
|
|
154
|
+
|
|
155
|
+
console.log(`${prefix} ${peer.ip.padEnd(16)} ${chalk.dim(peer.mac)}${gwBadge}${pyreBadge}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
console.log(' │');
|
|
159
|
+
|
|
160
|
+
// Active External Remote Sockets
|
|
161
|
+
console.log(` └── ${chalk.cyan.bold('TOP ACTIVE REMOTE CONNECTIONS')}`);
|
|
162
|
+
if (establishedConns.length === 0) {
|
|
163
|
+
console.log(` └── ${chalk.dim('No active established sockets detected.')}`);
|
|
164
|
+
} else {
|
|
165
|
+
for (let i = 0; i < establishedConns.length; i++) {
|
|
166
|
+
const conn = establishedConns[i];
|
|
167
|
+
const isLast = i === establishedConns.length - 1;
|
|
168
|
+
const prefix = isLast ? ' └──' : ' ├──';
|
|
169
|
+
const connsBadge = chalk.dim(`(${conn.connectionCount} socket${conn.connectionCount > 1 ? 's' : ''})`);
|
|
170
|
+
console.log(`${prefix} ${chalk.white(conn.host.padEnd(24))} ${chalk.green('ESTABLISHED')} ${connsBadge}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log();
|
|
175
|
+
console.log(chalk.dim(' Tip: Run "pyre p2p server" on any node to allow automatic live cluster streaming.\n'));
|
|
176
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pyre watchdog — Persistent Autonomous Rules Engine.
|
|
3
|
+
*
|
|
4
|
+
* Evaluates configurable metric threshold and process rules continuously,
|
|
5
|
+
* executing remediation actions (kill, renice) and dispatching multi-channel alerts.
|
|
6
|
+
*
|
|
7
|
+
* Example watchdog configuration rule:
|
|
8
|
+
* {
|
|
9
|
+
* "name": "Stop runaway background renderers",
|
|
10
|
+
* "metric": "cpu",
|
|
11
|
+
* "process": "photoanalysisd",
|
|
12
|
+
* "threshold": 80,
|
|
13
|
+
* "durationSec": 10,
|
|
14
|
+
* "action": "kill", // "kill" | "notify" | "alert"
|
|
15
|
+
* "cooldownSec": 60
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import chalk from 'chalk';
|
|
21
|
+
import { collectAll } from './monitors/collectors.js';
|
|
22
|
+
import { sendAlert } from './alertChannels.js';
|
|
23
|
+
import { getConfigDir, readConfig } from './state/config.js';
|
|
24
|
+
|
|
25
|
+
export interface WatchdogRule {
|
|
26
|
+
name: string;
|
|
27
|
+
metric?: 'cpu' | 'mem' | 'temp' | 'process_cpu' | 'process_mem';
|
|
28
|
+
process?: string;
|
|
29
|
+
threshold: number;
|
|
30
|
+
durationSec?: number;
|
|
31
|
+
action: 'kill' | 'notify' | 'alert';
|
|
32
|
+
cooldownSec?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface WatchdogOptions {
|
|
36
|
+
configFile?: string;
|
|
37
|
+
dryRun?: boolean;
|
|
38
|
+
interval?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const DEFAULT_WATCHDOG_RULES: WatchdogRule[] = [
|
|
42
|
+
{
|
|
43
|
+
name: 'High CPU System Alert',
|
|
44
|
+
metric: 'cpu',
|
|
45
|
+
threshold: 92,
|
|
46
|
+
durationSec: 5,
|
|
47
|
+
action: 'alert',
|
|
48
|
+
cooldownSec: 120,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'Critical Temperature Protection',
|
|
52
|
+
metric: 'temp',
|
|
53
|
+
threshold: 98,
|
|
54
|
+
durationSec: 5,
|
|
55
|
+
action: 'alert',
|
|
56
|
+
cooldownSec: 180,
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
export async function runWatchdogCommand(opts: WatchdogOptions = {}): Promise<void> {
|
|
61
|
+
const configDir = getConfigDir();
|
|
62
|
+
const defaultRulePath = path.join(configDir, 'watchdog.json');
|
|
63
|
+
const rulePath = opts.configFile || defaultRulePath;
|
|
64
|
+
const intervalSec = opts.interval || 2;
|
|
65
|
+
const isDryRun = !!opts.dryRun;
|
|
66
|
+
|
|
67
|
+
let rules: WatchdogRule[] = DEFAULT_WATCHDOG_RULES;
|
|
68
|
+
|
|
69
|
+
if (fs.existsSync(rulePath)) {
|
|
70
|
+
try {
|
|
71
|
+
rules = JSON.parse(fs.readFileSync(rulePath, 'utf-8'));
|
|
72
|
+
} catch (err: any) {
|
|
73
|
+
console.error(chalk.yellow(` ⚠ Could not parse watchdog config at ${rulePath}: ${err.message}. Using default rules.`));
|
|
74
|
+
}
|
|
75
|
+
} else if (!opts.configFile) {
|
|
76
|
+
try {
|
|
77
|
+
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
|
|
78
|
+
fs.writeFileSync(defaultRulePath, JSON.stringify(DEFAULT_WATCHDOG_RULES, null, 2));
|
|
79
|
+
} catch {
|
|
80
|
+
// ignore
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(chalk.bold('\n 🔥 pyre watchdog — Autonomous Monitoring Daemon\n'));
|
|
85
|
+
console.log(` ${chalk.green('✔')} Loaded ${chalk.bold(String(rules.length))} rule(s) from ${chalk.cyan(rulePath)}`);
|
|
86
|
+
console.log(` ${chalk.green('✔')} Refresh interval: ${intervalSec}s | Dry run mode: ${isDryRun ? chalk.yellow('ON') : chalk.dim('OFF')}`);
|
|
87
|
+
console.log(`\n ${chalk.dim('Active Rules:')}`);
|
|
88
|
+
for (const r of rules) {
|
|
89
|
+
const target = r.process ? `proc "${r.process}"` : r.metric || 'system';
|
|
90
|
+
console.log(` • ${chalk.bold(r.name)} [if ${target} > ${r.threshold} → ${chalk.cyan(r.action)}]`);
|
|
91
|
+
}
|
|
92
|
+
console.log(chalk.dim('\n Watching... Press Ctrl+C to stop.\n'));
|
|
93
|
+
|
|
94
|
+
// Track rule trigger history for cooldowns
|
|
95
|
+
const lastTriggered = new Map<string, number>();
|
|
96
|
+
|
|
97
|
+
const config = readConfig();
|
|
98
|
+
const alertChannels = {
|
|
99
|
+
slackUrl: config.slackAlertUrl,
|
|
100
|
+
discordUrl: config.discordAlertUrl,
|
|
101
|
+
pushoverToken: config.pushoverToken,
|
|
102
|
+
pushoverUser: config.pushoverUser,
|
|
103
|
+
ntfyUrl: config.ntfyUrl,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const evaluate = async () => {
|
|
107
|
+
try {
|
|
108
|
+
const data = await collectAll();
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
|
|
111
|
+
for (const rule of rules) {
|
|
112
|
+
const cooldown = (rule.cooldownSec || 60) * 1000;
|
|
113
|
+
const last = lastTriggered.get(rule.name) || 0;
|
|
114
|
+
if (now - last < cooldown) continue;
|
|
115
|
+
|
|
116
|
+
let triggered = false;
|
|
117
|
+
let detail = '';
|
|
118
|
+
let targetPid: number | null = null;
|
|
119
|
+
|
|
120
|
+
if (rule.process) {
|
|
121
|
+
const match = data.processes.find(p => p.command.toLowerCase().includes(rule.process!.toLowerCase()));
|
|
122
|
+
if (match) {
|
|
123
|
+
const val = rule.metric === 'process_mem' ? match.mem : match.cpu;
|
|
124
|
+
if (val >= rule.threshold) {
|
|
125
|
+
triggered = true;
|
|
126
|
+
targetPid = match.pid;
|
|
127
|
+
detail = `Process "${match.command}" (PID ${match.pid}) at ${val.toFixed(1)}% (threshold ${rule.threshold}%)`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} else if (rule.metric === 'cpu') {
|
|
131
|
+
if (data.cpu.usage >= rule.threshold) {
|
|
132
|
+
triggered = true;
|
|
133
|
+
detail = `System CPU usage at ${data.cpu.usage.toFixed(1)}% (threshold ${rule.threshold}%)`;
|
|
134
|
+
}
|
|
135
|
+
} else if (rule.metric === 'mem') {
|
|
136
|
+
if (data.memory.usagePercent >= rule.threshold) {
|
|
137
|
+
triggered = true;
|
|
138
|
+
detail = `System Memory at ${data.memory.usagePercent.toFixed(1)}% (threshold ${rule.threshold}%)`;
|
|
139
|
+
}
|
|
140
|
+
} else if (rule.metric === 'temp') {
|
|
141
|
+
const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die;
|
|
142
|
+
if (temp !== undefined && temp >= rule.threshold) {
|
|
143
|
+
triggered = true;
|
|
144
|
+
detail = `System Temperature at ${temp.toFixed(1)}°C (threshold ${rule.threshold}°C)`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (triggered) {
|
|
149
|
+
lastTriggered.set(rule.name, now);
|
|
150
|
+
const timeStr = new Date().toLocaleTimeString();
|
|
151
|
+
console.log(` [${chalk.dim(timeStr)}] ${chalk.red.bold('⚠ TRIGGERED:')} ${chalk.bold(rule.name)}: ${detail}`);
|
|
152
|
+
|
|
153
|
+
if (isDryRun) {
|
|
154
|
+
console.log(chalk.yellow(` [DRY RUN] Would execute action: ${rule.action}`));
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (rule.action === 'kill' && targetPid) {
|
|
159
|
+
try {
|
|
160
|
+
process.kill(targetPid, 'SIGTERM');
|
|
161
|
+
console.log(chalk.green(` ✔ Killed PID ${targetPid} with SIGTERM`));
|
|
162
|
+
} catch (err: any) {
|
|
163
|
+
console.log(chalk.red(` ✖ Failed to kill PID ${targetPid}: ${err.message}`));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (rule.action === 'alert') {
|
|
168
|
+
await sendAlert({
|
|
169
|
+
metric: rule.name,
|
|
170
|
+
value: rule.threshold,
|
|
171
|
+
threshold: rule.threshold,
|
|
172
|
+
unit: '%',
|
|
173
|
+
host: data.header.hostname,
|
|
174
|
+
timestamp: new Date().toISOString(),
|
|
175
|
+
severity: 'critical',
|
|
176
|
+
}, alertChannels).catch(() => {});
|
|
177
|
+
console.log(chalk.green(' ✔ Multi-channel alert dispatched.'));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
// ignore loop error
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
await evaluate();
|
|
187
|
+
const handle = setInterval(evaluate, intervalSec * 1000);
|
|
188
|
+
|
|
189
|
+
// Keep alive until SIGINT
|
|
190
|
+
await new Promise<void>((resolve) => {
|
|
191
|
+
process.on('SIGINT', () => {
|
|
192
|
+
clearInterval(handle);
|
|
193
|
+
console.log(chalk.dim('\n Watchdog stopped.'));
|
|
194
|
+
resolve();
|
|
195
|
+
process.exit(0);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
package/src/xbar.ts
CHANGED
|
@@ -6,16 +6,15 @@ import chalk from 'chalk';
|
|
|
6
6
|
export function generateXbarPlugin() {
|
|
7
7
|
const pyrePath = process.argv[1];
|
|
8
8
|
const nodePath = process.execPath;
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
const script = `#!/bin/bash
|
|
11
11
|
# <xbar.title>Pyre Monitor</xbar.title>
|
|
12
12
|
# <xbar.version>v1.0</xbar.version>
|
|
13
13
|
# <xbar.author>somalip</xbar.author>
|
|
14
14
|
# <xbar.author.github>somalip</xbar.author.github>
|
|
15
|
-
# <xbar.desc>
|
|
15
|
+
# <xbar.desc>Cross-platform system monitoring powered by pyre.</xbar.desc>
|
|
16
16
|
|
|
17
17
|
# Run pyre to get a JSON snapshot
|
|
18
|
-
# We use node directly to avoid env issues in xbar
|
|
19
18
|
DATA=$("${nodePath}" "${pyrePath}" --once --json)
|
|
20
19
|
|
|
21
20
|
if [ -z "$DATA" ]; then
|
|
@@ -39,15 +38,27 @@ echo "---"
|
|
|
39
38
|
echo "Open Dashboard | bash='${nodePath}' param1='${pyrePath}' param2='live' terminal=true"
|
|
40
39
|
`;
|
|
41
40
|
|
|
42
|
-
const pluginDir = path.join(os.homedir(), 'Library', 'Application Support', 'xbar', 'plugins');
|
|
43
41
|
const filename = 'pyre-monitor.2s.sh';
|
|
44
42
|
const outPath = path.join(process.cwd(), filename);
|
|
45
|
-
|
|
43
|
+
|
|
46
44
|
fs.writeFileSync(outPath, script);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
try {
|
|
46
|
+
fs.chmodSync(outPath, 0o755);
|
|
47
|
+
} catch {
|
|
48
|
+
// ignore on non-POSIX filesystems
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(chalk.green(`Generated menu bar plugin script at ${outPath}`));
|
|
52
|
+
|
|
53
|
+
if (process.platform === 'darwin') {
|
|
54
|
+
const pluginDir = path.join(os.homedir(), 'Library', 'Application Support', 'xbar', 'plugins');
|
|
55
|
+
console.log(`To install in xbar or SwiftBar on macOS:`);
|
|
56
|
+
console.log(chalk.cyan(` mv ${filename} "${pluginDir}/"`));
|
|
57
|
+
} else if (process.platform === 'linux') {
|
|
58
|
+
const argosDir = path.join(os.homedir(), '.config', 'argos');
|
|
59
|
+
console.log(`To install in Argos (GNOME Shell) on Linux:`);
|
|
60
|
+
console.log(chalk.cyan(` mkdir -p "${argosDir}" && mv ${filename} "${argosDir}/"`));
|
|
61
|
+
} else {
|
|
62
|
+
console.log(chalk.dim(`Compatible with BitBar, xbar, and Argos shell plugins.`));
|
|
63
|
+
}
|
|
53
64
|
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
run
|
|
3
|
-
} from "./chunk-FH5WDJBF.js";
|
|
4
|
-
|
|
5
|
-
// src/brewHealth.ts
|
|
6
|
-
import chalk from "chalk";
|
|
7
|
-
async function getBrewHealth() {
|
|
8
|
-
try {
|
|
9
|
-
const brewPath = await run("which brew 2>&1", "");
|
|
10
|
-
if (!brewPath.trim() || brewPath.includes("not found")) {
|
|
11
|
-
return { installedCount: 0, outdatedCount: 0, cellarSize: "N/A", doctorWarnings: [], brewAvailable: false };
|
|
12
|
-
}
|
|
13
|
-
const outdatedRaw = await run("brew outdated --quiet 2>&1", "");
|
|
14
|
-
const outdatedLines = outdatedRaw.trim().split("\n").filter((l) => l.length > 0 && !l.startsWith("Homebrew") && !l.startsWith("==>"));
|
|
15
|
-
const listRaw = await run("brew list --formula -1 2>&1", "");
|
|
16
|
-
const listLines = listRaw.trim().split("\n").filter((l) => l.length > 0);
|
|
17
|
-
let cellarSize = "N/A";
|
|
18
|
-
try {
|
|
19
|
-
const cellarPath = await run("brew --cellar 2>&1", "");
|
|
20
|
-
if (cellarPath.trim()) {
|
|
21
|
-
const duRaw = await run(`du -sh "${cellarPath.trim()}" 2>&1`, "");
|
|
22
|
-
const duMatch = duRaw.match(/^([0-9\.\,]+[KMGTP]?i?B?)/i);
|
|
23
|
-
if (duMatch) cellarSize = duMatch[1];
|
|
24
|
-
}
|
|
25
|
-
} catch {
|
|
26
|
-
cellarSize = "N/A";
|
|
27
|
-
}
|
|
28
|
-
const doctorRaw = await run("brew doctor 2>&1", "");
|
|
29
|
-
const doctorWarnings = [];
|
|
30
|
-
const docLines = doctorRaw.split("\n");
|
|
31
|
-
for (const line of docLines) {
|
|
32
|
-
if (line.startsWith("Warning:")) {
|
|
33
|
-
doctorWarnings.push(line.replace(/^Warning:\s*/, "").trim());
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return {
|
|
37
|
-
installedCount: listLines.length,
|
|
38
|
-
outdatedCount: outdatedLines.length,
|
|
39
|
-
cellarSize,
|
|
40
|
-
doctorWarnings,
|
|
41
|
-
brewAvailable: true
|
|
42
|
-
};
|
|
43
|
-
} catch (err) {
|
|
44
|
-
return { installedCount: 0, outdatedCount: 0, cellarSize: "N/A", doctorWarnings: [], brewAvailable: false };
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
async function printBrewHealthReport() {
|
|
48
|
-
console.log(chalk.bold("\n pyre brew \u2014 Homebrew Health Summary\n"));
|
|
49
|
-
const health = await getBrewHealth();
|
|
50
|
-
if (!health.brewAvailable) {
|
|
51
|
-
console.log(chalk.yellow(" \u26A0 Homebrew is not installed or not available on PATH.\n"));
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
console.log(` \u{1F4E6} Installed Formulae: ${chalk.bold(health.installedCount)}`);
|
|
55
|
-
console.log(` \u23F3 Outdated Packages: ${health.outdatedCount > 0 ? chalk.yellow.bold(health.outdatedCount) : chalk.green("0 (Up to date)")}`);
|
|
56
|
-
console.log(` \u{1F4BE} Cellar Disk Usage: ${chalk.bold(health.cellarSize)}`);
|
|
57
|
-
console.log();
|
|
58
|
-
if (health.doctorWarnings.length === 0) {
|
|
59
|
-
console.log(chalk.green(" \u2714 brew doctor: No warnings detected system-wide."));
|
|
60
|
-
} else {
|
|
61
|
-
console.log(chalk.yellow(` \u26A0 brew doctor warnings (${health.doctorWarnings.length}):`));
|
|
62
|
-
for (const warn of health.doctorWarnings.slice(0, 5)) {
|
|
63
|
-
console.log(chalk.dim(` \u2022 ${warn}`));
|
|
64
|
-
}
|
|
65
|
-
if (health.doctorWarnings.length > 5) {
|
|
66
|
-
console.log(chalk.dim(` ... and ${health.doctorWarnings.length - 5} more.`));
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
console.log();
|
|
70
|
-
}
|
|
71
|
-
export {
|
|
72
|
-
getBrewHealth,
|
|
73
|
-
printBrewHealthReport
|
|
74
|
-
};
|
package/dist/chunk-FH5WDJBF.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// src/monitors/run.ts
|
|
2
|
-
import { exec } from "child_process";
|
|
3
|
-
import { promisify } from "util";
|
|
4
|
-
var execAsync = promisify(exec);
|
|
5
|
-
async function run(cmd, fallback = "", timeout = 3e3) {
|
|
6
|
-
try {
|
|
7
|
-
return (await execAsync(cmd, { encoding: "utf8", timeout })).stdout.trim();
|
|
8
|
-
} catch {
|
|
9
|
-
return fallback;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export {
|
|
14
|
-
run
|
|
15
|
-
};
|