pyre-cli 9.5.2 → 10.1.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 (86) hide show
  1. package/README.md +73 -16
  2. package/dist/alertChannels-TPSQIJ7S.js +8 -0
  3. package/dist/{blender-7UT5J7IP.js → blender-FGH3ASEQ.js} +2 -2
  4. package/dist/brewHealth-P2GW5Y5C.js +176 -0
  5. package/dist/buildCmd-CDPZ2ZFA.js +38 -0
  6. package/dist/{checkCmd-T7O4KXXA.js → checkCmd-RKW6QGDR.js} +9 -11
  7. package/dist/{chunk-CXL4GYGQ.js → chunk-53O2RGSL.js} +44 -17
  8. package/dist/{chunk-3TE7MJLS.js → chunk-6UHZP4QP.js} +36 -10
  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-25I533OF.js → chunk-VIACN2RU.js} +1 -1
  14. package/dist/chunk-Z2YV5HKV.js +87 -0
  15. package/dist/chunk-ZLVIVFSO.js +110 -0
  16. package/dist/{collectors-JB4OGUEF.js → collectors-H43PN4PS.js} +6 -3
  17. package/dist/{completions-VQMHQCXE.js → completions-JKYEJNGR.js} +34 -1
  18. package/dist/{config-KS5ZZ3VN.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 +460 -88
  23. package/dist/ioreport.py +72 -0
  24. package/dist/netUsageCmd-WKBTW2YG.js +90 -0
  25. package/dist/{pipeCmd-TYLJHQ5B.js → pipeCmd-7REDBLBU.js} +4 -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-Z3RGA3VB.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/brewHealth.ts +157 -40
  44. package/src/buildCmd.ts +48 -0
  45. package/src/checkCmd.ts +5 -5
  46. package/src/completions.ts +35 -0
  47. package/src/doctor.ts +235 -7
  48. package/src/easy_features.test.ts +40 -0
  49. package/src/explainCmd.ts +261 -0
  50. package/src/extensions.ts +71 -7
  51. package/src/formatters/render.ts +34 -10
  52. package/src/formatters/types.ts +2 -0
  53. package/src/index.ts +251 -36
  54. package/src/live/input.ts +60 -0
  55. package/src/live/render.ts +67 -0
  56. package/src/live/state.ts +5 -0
  57. package/src/live/types.ts +1 -1
  58. package/src/monitors/batteryPredictor.ts +143 -0
  59. package/src/monitors/buildTracker.ts +121 -0
  60. package/src/monitors/collectors.ts +342 -178
  61. package/src/monitors/ioreport.py +72 -0
  62. package/src/monitors/platform/linux.ts +756 -0
  63. package/src/monitors/platform/windows.ts +519 -0
  64. package/src/monitors/run.ts +27 -3
  65. package/src/monitors/smc.ts +21 -1
  66. package/src/monitors/types.ts +15 -0
  67. package/src/netUsageCmd.ts +121 -0
  68. package/src/platform.test.ts +99 -0
  69. package/src/plugins/index.ts +143 -0
  70. package/src/profileProcCmd.ts +133 -0
  71. package/src/prometheusCmd.ts +107 -0
  72. package/src/replayCmd.ts +186 -0
  73. package/src/serveCmd.ts +180 -0
  74. package/src/service.ts +125 -7
  75. package/src/smartCmd.ts +205 -0
  76. package/src/state/config.ts +58 -17
  77. package/src/syncCmd.ts +166 -0
  78. package/src/topoCmd.ts +176 -0
  79. package/src/watchdogCmd.ts +198 -0
  80. package/src/xbar.ts +22 -11
  81. package/dist/brewHealth-F2BXF5M6.js +0 -74
  82. package/dist/chunk-FH5WDJBF.js +0 -15
  83. package/dist/chunk-N4WJVF5M.js +0 -1184
  84. package/dist/doctor-UV7TWIHX.js +0 -184
  85. package/dist/extensions-DVA5YLGH.js +0 -74
  86. package/dist/service-WTS3NBQU.js +0 -73
@@ -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
+ }
@@ -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 PLIST_LABEL = 'com.pyre.web';
8
- const PLIST_PATH = path.join(os.homedir(), 'Library', 'LaunchAgents', `${PLIST_LABEL}.plist`);
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 installLaunchdAgent(port: number = 3000): void {
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>${PLIST_LABEL}</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 if not previously loaded
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: ${PLIST_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
- export function uninstallLaunchdAgent(): void {
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
+ }