pyre-cli 9.5.2 → 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 +73 -16
- package/dist/alertChannels-TPSQIJ7S.js +8 -0
- package/dist/{blender-7UT5J7IP.js → blender-FGH3ASEQ.js} +2 -2
- package/dist/brewHealth-P2GW5Y5C.js +176 -0
- package/dist/buildCmd-CDPZ2ZFA.js +38 -0
- package/dist/{checkCmd-T7O4KXXA.js → checkCmd-RKW6QGDR.js} +9 -11
- package/dist/{chunk-CXL4GYGQ.js → chunk-53O2RGSL.js} +44 -17
- package/dist/{chunk-3TE7MJLS.js → chunk-6UHZP4QP.js} +36 -10
- 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-25I533OF.js → chunk-VIACN2RU.js} +1 -1
- package/dist/chunk-Z2YV5HKV.js +87 -0
- package/dist/chunk-ZLVIVFSO.js +110 -0
- package/dist/{collectors-JB4OGUEF.js → collectors-H43PN4PS.js} +6 -3
- package/dist/{completions-VQMHQCXE.js → completions-JKYEJNGR.js} +34 -1
- package/dist/{config-KS5ZZ3VN.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 +460 -88
- package/dist/ioreport.py +72 -0
- package/dist/netUsageCmd-WKBTW2YG.js +90 -0
- package/dist/{pipeCmd-TYLJHQ5B.js → pipeCmd-7REDBLBU.js} +4 -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-Z3RGA3VB.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/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/render.ts +34 -10
- package/src/formatters/types.ts +2 -0
- package/src/index.ts +251 -36
- package/src/live/input.ts +60 -0
- package/src/live/render.ts +67 -0
- package/src/live/state.ts +5 -0
- package/src/live/types.ts +1 -1
- package/src/monitors/batteryPredictor.ts +143 -0
- package/src/monitors/buildTracker.ts +121 -0
- package/src/monitors/collectors.ts +342 -178
- 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 +15 -0
- 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 +58 -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-N4WJVF5M.js +0 -1184
- package/dist/doctor-UV7TWIHX.js +0 -184
- package/dist/extensions-DVA5YLGH.js +0 -74
- package/dist/service-WTS3NBQU.js +0 -73
package/src/replayCmd.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pyre replay — CSV log file playback TUI.
|
|
3
|
+
*
|
|
4
|
+
* Reads a pyre CSV log file (from pyre live --log) and plays it back
|
|
5
|
+
* in a lightweight read-only TUI.
|
|
6
|
+
*
|
|
7
|
+
* Controls: Space/p=pause ]/[=speed arrow keys=step q=quit
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import readline from 'node:readline';
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
|
|
13
|
+
export interface ReplayOptions {
|
|
14
|
+
file: string;
|
|
15
|
+
speed?: number;
|
|
16
|
+
plain?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface LogRow {
|
|
20
|
+
timestamp: string;
|
|
21
|
+
cpuUsage: number;
|
|
22
|
+
memUsage: number;
|
|
23
|
+
tempC?: number;
|
|
24
|
+
rxBytes?: number;
|
|
25
|
+
txBytes?: number;
|
|
26
|
+
rxPackets?: number;
|
|
27
|
+
txPackets?: number;
|
|
28
|
+
connections?: number;
|
|
29
|
+
thermalState?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseLogFile(filePath: string): LogRow[] {
|
|
33
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
34
|
+
const rows: LogRow[] = [];
|
|
35
|
+
for (const line of content.split('\n').slice(1)) {
|
|
36
|
+
if (!line.trim()) continue;
|
|
37
|
+
const parts = line.split(',');
|
|
38
|
+
if (parts.length < 3) continue;
|
|
39
|
+
const cpuUsage = parseFloat(parts[1]);
|
|
40
|
+
const memUsage = parseFloat(parts[2]);
|
|
41
|
+
if (isNaN(cpuUsage) || isNaN(memUsage)) continue;
|
|
42
|
+
rows.push({
|
|
43
|
+
timestamp: parts[0]?.trim() ?? '',
|
|
44
|
+
cpuUsage,
|
|
45
|
+
memUsage,
|
|
46
|
+
tempC: parts[3] ? parseFloat(parts[3]) || undefined : undefined,
|
|
47
|
+
rxBytes: parts[4] ? parseFloat(parts[4]) || undefined : undefined,
|
|
48
|
+
txBytes: parts[5] ? parseFloat(parts[5]) || undefined : undefined,
|
|
49
|
+
rxPackets: parts[6] ? parseFloat(parts[6]) || undefined : undefined,
|
|
50
|
+
txPackets: parts[7] ? parseFloat(parts[7]) || undefined : undefined,
|
|
51
|
+
connections: parts[8] ? parseInt(parts[8], 10) || undefined : undefined,
|
|
52
|
+
thermalState: parts[9]?.trim() || undefined,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return rows;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function fmtBytes(bytes: number): string {
|
|
59
|
+
if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)} GB`;
|
|
60
|
+
if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(1)} MB`;
|
|
61
|
+
if (bytes >= 1e3) return `${(bytes / 1e3).toFixed(0)} KB`;
|
|
62
|
+
return `${bytes} B`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function bar(pct: number, width = 20): string {
|
|
66
|
+
const filled = Math.max(0, Math.min(Math.round((pct / 100) * width), width));
|
|
67
|
+
const color = pct >= 90 ? chalk.red : pct >= 70 ? chalk.yellow : chalk.green;
|
|
68
|
+
return color('█'.repeat(filled)) + chalk.dim('░'.repeat(width - filled));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function renderFrame(row: LogRow, idx: number, total: number, speed: number, paused: boolean, cols: number): string[] {
|
|
72
|
+
const lines: string[] = [];
|
|
73
|
+
const divider = chalk.dim('─'.repeat(Math.min(cols - 2, 78)));
|
|
74
|
+
|
|
75
|
+
lines.push('');
|
|
76
|
+
lines.push(chalk.bold.hex('#ff5500')(' 🔥 pyre replay') + ' ' + chalk.dim(row.timestamp || ''));
|
|
77
|
+
lines.push(' ' + divider);
|
|
78
|
+
lines.push('');
|
|
79
|
+
lines.push(` CPU ${bar(row.cpuUsage)} ${String(row.cpuUsage.toFixed(1)).padStart(5)}%`);
|
|
80
|
+
lines.push(` Memory ${bar(row.memUsage)} ${String(row.memUsage.toFixed(1)).padStart(5)}%`);
|
|
81
|
+
|
|
82
|
+
if (row.tempC !== undefined && !isNaN(row.tempC)) {
|
|
83
|
+
const tc = row.tempC >= 90 ? chalk.red : row.tempC >= 70 ? chalk.yellow : chalk.cyan;
|
|
84
|
+
lines.push(` Temp ${tc(`${row.tempC.toFixed(1)}°C`)}`);
|
|
85
|
+
}
|
|
86
|
+
if (row.rxBytes !== undefined || row.txBytes !== undefined) {
|
|
87
|
+
const rx = row.rxBytes !== undefined ? fmtBytes(row.rxBytes) : 'N/A';
|
|
88
|
+
const tx = row.txBytes !== undefined ? fmtBytes(row.txBytes) : 'N/A';
|
|
89
|
+
lines.push(` Network ↓ ${chalk.green(rx)} ↑ ${chalk.blue(tx)}`);
|
|
90
|
+
}
|
|
91
|
+
if (row.connections !== undefined) {
|
|
92
|
+
lines.push(` Conns ${chalk.cyan(String(row.connections))}`);
|
|
93
|
+
}
|
|
94
|
+
if (row.thermalState && row.thermalState !== 'Nominal') {
|
|
95
|
+
const sc = row.thermalState === 'Critical' ? chalk.red : chalk.yellow;
|
|
96
|
+
lines.push(` Thermal ${sc(row.thermalState)}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
lines.push('');
|
|
100
|
+
lines.push(' ' + divider);
|
|
101
|
+
|
|
102
|
+
// Scrub bar
|
|
103
|
+
const progWidth = 30;
|
|
104
|
+
const progress = total > 1 ? Math.round((idx / (total - 1)) * progWidth) : 0;
|
|
105
|
+
const progressBar = chalk.cyan('█'.repeat(progress)) + chalk.dim('░'.repeat(progWidth - progress));
|
|
106
|
+
const icon = paused ? chalk.yellow('⏸') : chalk.green('▶');
|
|
107
|
+
const spStr = speed === 1 ? '1×' : `${speed}×`;
|
|
108
|
+
lines.push(` ${icon} ${progressBar} ${chalk.bold(`${idx + 1}/${total}`)} ${chalk.dim(spStr)}`);
|
|
109
|
+
lines.push('');
|
|
110
|
+
lines.push(chalk.dim(' Space/p=pause ]/[=speed ←/→=step q=quit'));
|
|
111
|
+
|
|
112
|
+
return lines;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function runReplayCommand(opts: ReplayOptions): Promise<void> {
|
|
116
|
+
const { file, speed: initialSpeed = 1, plain } = opts;
|
|
117
|
+
|
|
118
|
+
if (!fs.existsSync(file)) {
|
|
119
|
+
console.error(chalk.red(` ✖ File not found: ${file}`));
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let rows: LogRow[];
|
|
124
|
+
try {
|
|
125
|
+
rows = parseLogFile(file);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.error(chalk.red(` ✖ Could not parse log file: ${err instanceof Error ? err.message : String(err)}`));
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (rows.length === 0) {
|
|
132
|
+
console.log(chalk.yellow(' No data rows found in the log file.'));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (plain) chalk.level = 0;
|
|
137
|
+
|
|
138
|
+
let frameIdx = 0;
|
|
139
|
+
let speed = initialSpeed;
|
|
140
|
+
let paused = false;
|
|
141
|
+
let running = true;
|
|
142
|
+
|
|
143
|
+
process.stdin.setRawMode(true);
|
|
144
|
+
process.stdin.resume();
|
|
145
|
+
readline.emitKeypressEvents(process.stdin);
|
|
146
|
+
|
|
147
|
+
const cols = process.stdout.columns || 80;
|
|
148
|
+
process.stdout.write('\x1b[?25l\x1b[2J\x1b[H');
|
|
149
|
+
|
|
150
|
+
function renderCurrent() {
|
|
151
|
+
const lines = renderFrame(rows[frameIdx], frameIdx, rows.length, speed, paused, cols);
|
|
152
|
+
process.stdout.write('\x1b[H' + lines.map(l => `\x1b[2K${l}`).join('\r\n') + '\x1b[0J');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
renderCurrent();
|
|
156
|
+
|
|
157
|
+
process.stdin.on('keypress', (str: string, key: readline.Key) => {
|
|
158
|
+
if (!key) return;
|
|
159
|
+
if ((key.ctrl && key.name === 'c') || key.name === 'q') {
|
|
160
|
+
running = false;
|
|
161
|
+
process.stdout.write('\x1b[?25h\x1b[2J\x1b[H');
|
|
162
|
+
process.stdin.setRawMode(false);
|
|
163
|
+
process.stdin.pause();
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
if (key.name === 'space' || key.name === 'p') { paused = !paused; renderCurrent(); return; }
|
|
167
|
+
if (str === ']') { speed = Math.min(speed * 2, 32); renderCurrent(); return; }
|
|
168
|
+
if (str === '[') { speed = Math.max(speed / 2, 0.125); renderCurrent(); return; }
|
|
169
|
+
if (key.name === 'right') { frameIdx = Math.min(frameIdx + 1, rows.length - 1); renderCurrent(); return; }
|
|
170
|
+
if (key.name === 'left') { frameIdx = Math.max(frameIdx - 1, 0); renderCurrent(); return; }
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const BASE_MS = 2000;
|
|
174
|
+
const tick = () => {
|
|
175
|
+
if (!running) return;
|
|
176
|
+
if (!paused) {
|
|
177
|
+
frameIdx++;
|
|
178
|
+
if (frameIdx >= rows.length) { paused = true; frameIdx = rows.length - 1; }
|
|
179
|
+
renderCurrent();
|
|
180
|
+
}
|
|
181
|
+
setTimeout(tick, BASE_MS / speed);
|
|
182
|
+
};
|
|
183
|
+
setTimeout(tick, BASE_MS / speed);
|
|
184
|
+
|
|
185
|
+
await new Promise<void>(() => {});
|
|
186
|
+
}
|
package/src/serveCmd.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pyre serve — Dedicated REST API Mode.
|
|
3
|
+
*
|
|
4
|
+
* Exposes all metrics as structured JSON REST endpoints with optional
|
|
5
|
+
* API key authorization and CORS support.
|
|
6
|
+
*
|
|
7
|
+
* Endpoints:
|
|
8
|
+
* GET /api/all
|
|
9
|
+
* GET /api/cpu
|
|
10
|
+
* GET /api/memory
|
|
11
|
+
* GET /api/gpu
|
|
12
|
+
* GET /api/battery
|
|
13
|
+
* GET /api/thermal
|
|
14
|
+
* GET /api/network
|
|
15
|
+
* GET /api/disk
|
|
16
|
+
* GET /api/processes
|
|
17
|
+
* GET /api/containers
|
|
18
|
+
* GET /api/system
|
|
19
|
+
*/
|
|
20
|
+
import http from 'node:http';
|
|
21
|
+
import os from 'node:os';
|
|
22
|
+
import chalk from 'chalk';
|
|
23
|
+
import {
|
|
24
|
+
collectAll,
|
|
25
|
+
collectCpu,
|
|
26
|
+
collectMemory,
|
|
27
|
+
collectGpu,
|
|
28
|
+
collectBattery,
|
|
29
|
+
collectThermal,
|
|
30
|
+
collectNetwork,
|
|
31
|
+
collectDisk,
|
|
32
|
+
collectProcesses,
|
|
33
|
+
collectContainers,
|
|
34
|
+
collectSystem,
|
|
35
|
+
} from './monitors/collectors.js';
|
|
36
|
+
|
|
37
|
+
export interface ServeOptions {
|
|
38
|
+
port?: number;
|
|
39
|
+
apiKey?: string;
|
|
40
|
+
detailed?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function runServeCommand(opts: ServeOptions = {}): Promise<void> {
|
|
44
|
+
const port = opts.port || 8080;
|
|
45
|
+
const apiKey = opts.apiKey || process.env.PYRE_API_KEY || '';
|
|
46
|
+
|
|
47
|
+
const server = http.createServer(async (req, res) => {
|
|
48
|
+
// CORS headers
|
|
49
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
50
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
|
51
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-api-key');
|
|
52
|
+
|
|
53
|
+
if (req.method === 'OPTIONS') {
|
|
54
|
+
res.writeHead(204);
|
|
55
|
+
res.end();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const urlObj = new URL(req.url || '/', `http://localhost:${port}`);
|
|
60
|
+
const pathname = urlObj.pathname;
|
|
61
|
+
|
|
62
|
+
// Optional API key validation
|
|
63
|
+
if (apiKey) {
|
|
64
|
+
const authHeader = req.headers['authorization'] || '';
|
|
65
|
+
const xApiKey = req.headers['x-api-key'] || '';
|
|
66
|
+
const queryKey = urlObj.searchParams.get('key') || '';
|
|
67
|
+
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : authHeader;
|
|
68
|
+
|
|
69
|
+
if (token !== apiKey && xApiKey !== apiKey && queryKey !== apiKey) {
|
|
70
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
71
|
+
res.end(JSON.stringify({ error: 'Unauthorized: Invalid or missing API key' }));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
let data: any;
|
|
78
|
+
|
|
79
|
+
switch (pathname) {
|
|
80
|
+
case '/api/all':
|
|
81
|
+
case '/api/stats':
|
|
82
|
+
case '/api':
|
|
83
|
+
data = await collectAll({ detailed: opts.detailed });
|
|
84
|
+
break;
|
|
85
|
+
case '/api/cpu':
|
|
86
|
+
data = await collectCpu();
|
|
87
|
+
break;
|
|
88
|
+
case '/api/memory':
|
|
89
|
+
case '/api/mem':
|
|
90
|
+
data = await collectMemory();
|
|
91
|
+
break;
|
|
92
|
+
case '/api/gpu':
|
|
93
|
+
data = await collectGpu(opts.detailed);
|
|
94
|
+
break;
|
|
95
|
+
case '/api/battery':
|
|
96
|
+
case '/api/power':
|
|
97
|
+
data = await collectBattery();
|
|
98
|
+
break;
|
|
99
|
+
case '/api/thermal':
|
|
100
|
+
case '/api/temp':
|
|
101
|
+
data = await collectThermal(opts.detailed);
|
|
102
|
+
break;
|
|
103
|
+
case '/api/network':
|
|
104
|
+
case '/api/net':
|
|
105
|
+
data = await collectNetwork();
|
|
106
|
+
break;
|
|
107
|
+
case '/api/disk':
|
|
108
|
+
data = await collectDisk();
|
|
109
|
+
break;
|
|
110
|
+
case '/api/processes':
|
|
111
|
+
case '/api/proc':
|
|
112
|
+
data = await collectProcesses(50);
|
|
113
|
+
break;
|
|
114
|
+
case '/api/containers':
|
|
115
|
+
data = await collectContainers();
|
|
116
|
+
break;
|
|
117
|
+
case '/api/system':
|
|
118
|
+
case '/api/info':
|
|
119
|
+
data = await collectSystem();
|
|
120
|
+
break;
|
|
121
|
+
case '/health':
|
|
122
|
+
case '/api/health':
|
|
123
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
124
|
+
res.end(JSON.stringify({ status: 'ok', uptime: os.uptime(), timestamp: new Date().toISOString() }));
|
|
125
|
+
return;
|
|
126
|
+
default:
|
|
127
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
128
|
+
res.end(JSON.stringify({
|
|
129
|
+
error: 'Not found',
|
|
130
|
+
availableEndpoints: [
|
|
131
|
+
'/api/all',
|
|
132
|
+
'/api/cpu',
|
|
133
|
+
'/api/memory',
|
|
134
|
+
'/api/gpu',
|
|
135
|
+
'/api/battery',
|
|
136
|
+
'/api/thermal',
|
|
137
|
+
'/api/network',
|
|
138
|
+
'/api/disk',
|
|
139
|
+
'/api/processes',
|
|
140
|
+
'/api/containers',
|
|
141
|
+
'/api/system',
|
|
142
|
+
'/api/health',
|
|
143
|
+
],
|
|
144
|
+
}));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
149
|
+
res.end(JSON.stringify(data, null, 2));
|
|
150
|
+
} catch (err: any) {
|
|
151
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
152
|
+
res.end(JSON.stringify({ error: err.message || 'Internal metric collection error' }));
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
server.on('error', (err: any) => {
|
|
157
|
+
if (err.code === 'EADDRINUSE') {
|
|
158
|
+
console.error(chalk.red(`\n ✖ Error: Port ${port} is already in use. Use --port <port> to select an alternate port.\n`));
|
|
159
|
+
} else {
|
|
160
|
+
console.error(chalk.red(`\n ✖ Server error: ${err.message}\n`));
|
|
161
|
+
}
|
|
162
|
+
process.exit(1);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await new Promise<void>((resolve) => server.listen(port, '0.0.0.0', resolve));
|
|
166
|
+
|
|
167
|
+
console.log(chalk.bold('\n 🔥 pyre serve — REST API Server\n'));
|
|
168
|
+
console.log(` ${chalk.green('✔')} API base URL: ${chalk.cyan(`http://0.0.0.0:${port}/api/all`)}`);
|
|
169
|
+
console.log(` ${chalk.green('✔')} Health check: ${chalk.cyan(`http://0.0.0.0:${port}/api/health`)}`);
|
|
170
|
+
if (apiKey) {
|
|
171
|
+
console.log(` ${chalk.yellow('🔒')} API Key protection: ${chalk.bold('ENABLED')} (Pass via header x-api-key or ?key=)`);
|
|
172
|
+
} else {
|
|
173
|
+
console.log(` ${chalk.dim('🔓')} API Key protection: ${chalk.dim('DISABLED (Public local network access)')}`);
|
|
174
|
+
}
|
|
175
|
+
console.log(`\n ${chalk.dim('Endpoints: /api/cpu, /api/memory, /api/gpu, /api/battery, /api/thermal, /api/network, /api/disk, /api/processes')}`);
|
|
176
|
+
console.log(chalk.dim('\n Press Ctrl+C to stop.\n'));
|
|
177
|
+
|
|
178
|
+
// Keep alive
|
|
179
|
+
await new Promise<void>(() => {});
|
|
180
|
+
}
|
package/src/service.ts
CHANGED
|
@@ -4,10 +4,43 @@ import path from 'node:path';
|
|
|
4
4
|
import { execSync } from 'node:child_process';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
const PLIST_PATH = path.join(os.homedir(), 'Library', 'LaunchAgents', `${
|
|
7
|
+
const SERVICE_LABEL = 'com.pyre.web';
|
|
8
|
+
const PLIST_PATH = path.join(os.homedir(), 'Library', 'LaunchAgents', `${SERVICE_LABEL}.plist`);
|
|
9
|
+
const SYSTEMD_USER_DIR = path.join(os.homedir(), '.config', 'systemd', 'user');
|
|
10
|
+
const SYSTEMD_UNIT_PATH = path.join(SYSTEMD_USER_DIR, 'pyre-web.service');
|
|
11
|
+
const TASK_NAME = 'PyreWeb';
|
|
9
12
|
|
|
10
|
-
export function
|
|
13
|
+
export function installService(port: number = 3000): void {
|
|
14
|
+
const platform = process.platform;
|
|
15
|
+
if (platform === 'darwin') {
|
|
16
|
+
installLaunchd(port);
|
|
17
|
+
} else if (platform === 'linux') {
|
|
18
|
+
installSystemd(port);
|
|
19
|
+
} else if (platform === 'win32') {
|
|
20
|
+
installWindowsTask(port);
|
|
21
|
+
} else {
|
|
22
|
+
console.log(chalk.yellow(`⚠ Background service installation not supported on ${platform}.`));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function uninstallService(): void {
|
|
27
|
+
const platform = process.platform;
|
|
28
|
+
if (platform === 'darwin') {
|
|
29
|
+
uninstallLaunchd();
|
|
30
|
+
} else if (platform === 'linux') {
|
|
31
|
+
uninstallSystemd();
|
|
32
|
+
} else if (platform === 'win32') {
|
|
33
|
+
uninstallWindowsTask();
|
|
34
|
+
} else {
|
|
35
|
+
console.log(chalk.yellow(`⚠ Background service uninstallation not supported on ${platform}.`));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Backward-compatible exports for existing callers
|
|
40
|
+
export const installLaunchdAgent = installService;
|
|
41
|
+
export const uninstallLaunchdAgent = uninstallService;
|
|
42
|
+
|
|
43
|
+
function installLaunchd(port: number): void {
|
|
11
44
|
const nodePath = process.execPath;
|
|
12
45
|
const scriptPath = path.resolve(process.argv[1]);
|
|
13
46
|
|
|
@@ -16,7 +49,7 @@ export function installLaunchdAgent(port: number = 3000): void {
|
|
|
16
49
|
<plist version="1.0">
|
|
17
50
|
<dict>
|
|
18
51
|
<key>Label</key>
|
|
19
|
-
<string>${
|
|
52
|
+
<string>${SERVICE_LABEL}</string>
|
|
20
53
|
<key>ProgramArguments</key>
|
|
21
54
|
<array>
|
|
22
55
|
<string>${nodePath}</string>
|
|
@@ -47,18 +80,18 @@ export function installLaunchdAgent(port: number = 3000): void {
|
|
|
47
80
|
try {
|
|
48
81
|
execSync(`launchctl unload ${PLIST_PATH} 2>/dev/null`);
|
|
49
82
|
} catch {
|
|
50
|
-
// ignore
|
|
83
|
+
// ignore
|
|
51
84
|
}
|
|
52
85
|
execSync(`launchctl load ${PLIST_PATH}`);
|
|
53
86
|
|
|
54
87
|
console.log(chalk.green(`✔ Installed and started launchd agent at ${PLIST_PATH}`));
|
|
55
|
-
console.log(chalk.dim(` Service label: ${
|
|
88
|
+
console.log(chalk.dim(` Service label: ${SERVICE_LABEL}`));
|
|
56
89
|
} catch (err: any) {
|
|
57
90
|
console.log(chalk.red(`✖ Failed to install launchd agent: ${err.message}`));
|
|
58
91
|
}
|
|
59
92
|
}
|
|
60
93
|
|
|
61
|
-
|
|
94
|
+
function uninstallLaunchd(): void {
|
|
62
95
|
try {
|
|
63
96
|
if (fs.existsSync(PLIST_PATH)) {
|
|
64
97
|
try {
|
|
@@ -75,3 +108,88 @@ export function uninstallLaunchdAgent(): void {
|
|
|
75
108
|
console.log(chalk.red(`✖ Failed to uninstall launchd agent: ${err.message}`));
|
|
76
109
|
}
|
|
77
110
|
}
|
|
111
|
+
|
|
112
|
+
function installSystemd(port: number): void {
|
|
113
|
+
const nodePath = process.execPath;
|
|
114
|
+
const scriptPath = path.resolve(process.argv[1]);
|
|
115
|
+
const logDir = path.join(os.homedir(), '.config', 'pyre');
|
|
116
|
+
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
|
|
117
|
+
if (!fs.existsSync(SYSTEMD_USER_DIR)) fs.mkdirSync(SYSTEMD_USER_DIR, { recursive: true });
|
|
118
|
+
|
|
119
|
+
const unitContent = `[Unit]
|
|
120
|
+
Description=Pyre System Monitor Web Service
|
|
121
|
+
After=network.target
|
|
122
|
+
|
|
123
|
+
[Service]
|
|
124
|
+
Type=simple
|
|
125
|
+
ExecStart=${nodePath} "${scriptPath}" web --port ${port}
|
|
126
|
+
Restart=always
|
|
127
|
+
RestartSec=5
|
|
128
|
+
StandardOutput=append:${path.join(logDir, 'web.log')}
|
|
129
|
+
StandardError=append:${path.join(logDir, 'web.err')}
|
|
130
|
+
|
|
131
|
+
[Install]
|
|
132
|
+
WantedBy=default.target
|
|
133
|
+
`;
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
fs.writeFileSync(SYSTEMD_UNIT_PATH, unitContent);
|
|
137
|
+
try {
|
|
138
|
+
execSync('systemctl --user daemon-reload');
|
|
139
|
+
execSync('systemctl --user enable --now pyre-web.service');
|
|
140
|
+
} catch (cmdErr: any) {
|
|
141
|
+
console.log(chalk.yellow(` Unit file written to ${SYSTEMD_UNIT_PATH}, but could not auto-enable: ${cmdErr.message}`));
|
|
142
|
+
console.log(chalk.dim(' You can manually enable it using: systemctl --user enable --now pyre-web.service'));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
console.log(chalk.green(`✔ Installed and started systemd user service at ${SYSTEMD_UNIT_PATH}`));
|
|
146
|
+
console.log(chalk.dim(' Service name: pyre-web.service'));
|
|
147
|
+
} catch (err: any) {
|
|
148
|
+
console.log(chalk.red(`✖ Failed to install systemd user service: ${err.message}`));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function uninstallSystemd(): void {
|
|
153
|
+
try {
|
|
154
|
+
if (fs.existsSync(SYSTEMD_UNIT_PATH)) {
|
|
155
|
+
try {
|
|
156
|
+
execSync('systemctl --user disable --now pyre-web.service 2>/dev/null');
|
|
157
|
+
} catch {
|
|
158
|
+
// ignore
|
|
159
|
+
}
|
|
160
|
+
fs.unlinkSync(SYSTEMD_UNIT_PATH);
|
|
161
|
+
try {
|
|
162
|
+
execSync('systemctl --user daemon-reload 2>/dev/null');
|
|
163
|
+
} catch {
|
|
164
|
+
// ignore
|
|
165
|
+
}
|
|
166
|
+
console.log(chalk.green(`✔ Uninstalled systemd user service ${SYSTEMD_UNIT_PATH}`));
|
|
167
|
+
} else {
|
|
168
|
+
console.log(chalk.yellow(` No systemd service found at ${SYSTEMD_UNIT_PATH}`));
|
|
169
|
+
}
|
|
170
|
+
} catch (err: any) {
|
|
171
|
+
console.log(chalk.red(`✖ Failed to uninstall systemd service: ${err.message}`));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function installWindowsTask(port: number): void {
|
|
176
|
+
const nodePath = process.execPath;
|
|
177
|
+
const scriptPath = path.resolve(process.argv[1]);
|
|
178
|
+
try {
|
|
179
|
+
const cmd = `schtasks /create /tn "${TASK_NAME}" /tr "\\"${nodePath}\\" \\"${scriptPath}\\" web --port ${port}" /sc onlogon /f`;
|
|
180
|
+
execSync(cmd, { stdio: 'pipe' });
|
|
181
|
+
console.log(chalk.green(`✔ Installed Windows scheduled task '${TASK_NAME}' (runs on logon)`));
|
|
182
|
+
} catch (err: any) {
|
|
183
|
+
console.log(chalk.red(`✖ Failed to register Windows scheduled task: ${err.message}`));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function uninstallWindowsTask(): void {
|
|
188
|
+
try {
|
|
189
|
+
const cmd = `schtasks /delete /tn "${TASK_NAME}" /f`;
|
|
190
|
+
execSync(cmd, { stdio: 'pipe' });
|
|
191
|
+
console.log(chalk.green(`✔ Uninstalled Windows scheduled task '${TASK_NAME}'`));
|
|
192
|
+
} catch (err: any) {
|
|
193
|
+
console.log(chalk.yellow(` No scheduled task '${TASK_NAME}' found or deletion failed: ${err.message}`));
|
|
194
|
+
}
|
|
195
|
+
}
|