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,121 @@
1
+ /**
2
+ * pyre netusage — Per-application network usage tracker.
3
+ *
4
+ * Uses macOS socket accounting and nettop parsing from collectPackets
5
+ * to produce detailed real-time and cumulative network metrics per process.
6
+ */
7
+ import chalk from 'chalk';
8
+ import { collectPackets } from './monitors/collectors.js';
9
+ import { formatBytes } from './formatters/render.js';
10
+
11
+ export interface NetUsageOptions {
12
+ top?: number;
13
+ sort?: 'rx' | 'tx' | 'total';
14
+ json?: boolean;
15
+ interval?: number;
16
+ }
17
+
18
+ export async function runNetUsageCommand(opts: NetUsageOptions = {}): Promise<void> {
19
+ const topCount = opts.top || 15;
20
+ const sortKey = opts.sort || 'total';
21
+
22
+ const packetData = await collectPackets();
23
+ if (!packetData || !packetData.allProcesses || packetData.allProcesses.length === 0) {
24
+ if (opts.json) {
25
+ console.log(JSON.stringify({ processes: [], totalRx: 0, totalTx: 0 }));
26
+ return;
27
+ }
28
+ console.log(chalk.yellow('\n ⚠ No active per-application network sockets detected.'));
29
+ console.log(chalk.dim(' Ensure network activity is present or run with elevated permissions if required.\n'));
30
+ return;
31
+ }
32
+
33
+ // Aggregate by process command / name
34
+ interface AggregatedProc {
35
+ pid: number;
36
+ command: string;
37
+ rxBytes: number;
38
+ txBytes: number;
39
+ totalBytes: number;
40
+ connections: number;
41
+ }
42
+
43
+ const appMap = new Map<string, AggregatedProc>();
44
+
45
+ for (const proc of packetData.allProcesses) {
46
+ const key = proc.command || `pid-${proc.pid}`;
47
+ const rx = proc.rxBytes || 0;
48
+ const tx = proc.txBytes || 0;
49
+ const conns = proc.connections ? proc.connections.length : 0;
50
+
51
+ const existing = appMap.get(key);
52
+ if (existing) {
53
+ existing.rxBytes += rx;
54
+ existing.txBytes += tx;
55
+ existing.totalBytes += (rx + tx);
56
+ existing.connections += conns;
57
+ } else {
58
+ appMap.set(key, {
59
+ pid: proc.pid,
60
+ command: key,
61
+ rxBytes: rx,
62
+ txBytes: tx,
63
+ totalBytes: rx + tx,
64
+ connections: conns,
65
+ });
66
+ }
67
+ }
68
+
69
+ const sorted = Array.from(appMap.values()).sort((a, b) => {
70
+ if (sortKey === 'rx') return b.rxBytes - a.rxBytes;
71
+ if (sortKey === 'tx') return b.txBytes - a.txBytes;
72
+ return b.totalBytes - a.totalBytes;
73
+ });
74
+
75
+ const displayList = sorted.slice(0, topCount);
76
+
77
+ if (opts.json) {
78
+ console.log(JSON.stringify({
79
+ totalRx: packetData.rxPackets,
80
+ totalTx: packetData.txPackets,
81
+ activeConnections: packetData.connections,
82
+ processes: displayList,
83
+ }, null, 2));
84
+ return;
85
+ }
86
+
87
+ console.log(chalk.bold('\n 🔥 pyre netusage — Per-Application Network Traffic\n'));
88
+
89
+ const pidW = 8;
90
+ const appW = 28;
91
+ const rxW = 14;
92
+ const txW = 14;
93
+ const totalW = 14;
94
+ const connsW = 8;
95
+
96
+ const header =
97
+ chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' PID ') +
98
+ chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' APPLICATION ') +
99
+ chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' DOWN (RX) ') +
100
+ chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' UP (TX) ') +
101
+ chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' TOTAL ') +
102
+ chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' CONNS ');
103
+
104
+ console.log(' ' + header);
105
+ console.log(' ' + chalk.dim('─'.repeat(pidW + appW + rxW + txW + totalW + connsW)));
106
+
107
+ for (const p of displayList) {
108
+ const pidStr = String(p.pid).padEnd(pidW);
109
+ const appStr = (p.command.length > appW - 2 ? p.command.slice(0, appW - 3) + '…' : p.command).padEnd(appW);
110
+ const rxStr = chalk.green(formatBytes(p.rxBytes).padStart(rxW - 2)) + ' ';
111
+ const txStr = chalk.cyan(formatBytes(p.txBytes).padStart(txW - 2)) + ' ';
112
+ const totStr = chalk.bold(formatBytes(p.totalBytes).padStart(totalW - 2)) + ' ';
113
+ const connsStr = String(p.connections).padStart(connsW - 2);
114
+
115
+ console.log(` ${pidStr}${appStr}${rxStr}${txStr}${totStr}${connsStr}`);
116
+ }
117
+
118
+ console.log();
119
+ console.log(chalk.dim(` Total active socket connections: ${chalk.bold(String(packetData.connections))} | Showing top ${displayList.length} apps`));
120
+ console.log();
121
+ }
@@ -0,0 +1,99 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ describe('Linux Telemetry Parsers', () => {
5
+ it('should parse Linux memory info structure correctly', async () => {
6
+ const { collectLinuxMemory } = await import('./monitors/platform/linux.js');
7
+ const mem = await collectLinuxMemory();
8
+ assert.ok(typeof mem.total === 'number');
9
+ assert.ok(typeof mem.used === 'number');
10
+ assert.ok(typeof mem.free === 'number');
11
+ assert.ok(typeof mem.usagePercent === 'number');
12
+ assert.ok(mem.pageSize === 4096);
13
+ });
14
+
15
+ it('should collect Linux system info', async () => {
16
+ const { collectLinuxSystem } = await import('./monitors/platform/linux.js');
17
+ const sys = await collectLinuxSystem();
18
+ assert.ok(typeof sys.hostname === 'string');
19
+ assert.ok(typeof sys.os === 'string');
20
+ assert.ok(typeof sys.uptime === 'string');
21
+ });
22
+
23
+ it('should collect Linux disk structure', async () => {
24
+ const { collectLinuxDisk } = await import('./monitors/platform/linux.js');
25
+ const disks = await collectLinuxDisk();
26
+ assert.ok(Array.isArray(disks));
27
+ assert.ok(disks.length > 0);
28
+ assert.ok(typeof disks[0].filesystem === 'string');
29
+ assert.ok(typeof disks[0].mountpoint === 'string');
30
+ });
31
+
32
+ it('should collect Linux thermal structure gracefully', async () => {
33
+ const { collectLinuxThermal } = await import('./monitors/platform/linux.js');
34
+ const thermal = await collectLinuxThermal();
35
+ assert.ok(typeof thermal.state === 'string');
36
+ assert.ok(typeof thermal.pressureLevel === 'number');
37
+ });
38
+ });
39
+
40
+ describe('Windows Telemetry Parsers', () => {
41
+ it('should collect Windows system info with Windows OS mapping', async () => {
42
+ const { collectWindowsSystem } = await import('./monitors/platform/windows.js');
43
+ const sys = await collectWindowsSystem();
44
+ assert.ok(typeof sys.hostname === 'string');
45
+ assert.ok(sys.os.startsWith('Windows'));
46
+ assert.ok(typeof sys.uptime === 'string');
47
+ });
48
+
49
+ it('should collect Windows memory structure', async () => {
50
+ const { collectWindowsMemory } = await import('./monitors/platform/windows.js');
51
+ const mem = await collectWindowsMemory();
52
+ assert.ok(typeof mem.total === 'number');
53
+ assert.ok(typeof mem.used === 'number');
54
+ assert.ok(typeof mem.free === 'number');
55
+ assert.ok(typeof mem.usagePercent === 'number');
56
+ assert.ok(mem.pageSize === 4096);
57
+ });
58
+
59
+ it('should collect Windows CPU structure with simulated load average', async () => {
60
+ const { collectWindowsCpu } = await import('./monitors/platform/windows.js');
61
+ const cpuRef = { current: null };
62
+ const cpu = await collectWindowsCpu(cpuRef);
63
+ assert.ok(typeof cpu.brand === 'string');
64
+ assert.ok(cpu.cores >= 1);
65
+ assert.ok(Array.isArray(cpu.loadAvg));
66
+ assert.strictEqual(cpu.loadAvg.length, 3);
67
+ });
68
+
69
+ it('should return Windows disk fallback structure', async () => {
70
+ const { collectWindowsDisk } = await import('./monitors/platform/windows.js');
71
+ const disks = await collectWindowsDisk();
72
+ assert.ok(Array.isArray(disks));
73
+ assert.ok(disks.length > 0);
74
+ assert.ok(disks[0].mountpoint.includes(':'));
75
+ });
76
+
77
+ it('should collect Windows thermal structure with pressure level', async () => {
78
+ const { collectWindowsThermal } = await import('./monitors/platform/windows.js');
79
+ const thermal = await collectWindowsThermal();
80
+ assert.ok(typeof thermal.state === 'string');
81
+ assert.ok([0, 1, 2, 3].includes(thermal.pressureLevel));
82
+ });
83
+ });
84
+
85
+ describe('Cross-Platform Service & Package Management', () => {
86
+ it('should report package manager health without crashing', async () => {
87
+ const { getBrewHealth } = await import('./brewHealth.js');
88
+ const health = await getBrewHealth();
89
+ assert.ok(typeof health.managerName === 'string');
90
+ assert.ok(typeof health.brewAvailable === 'boolean');
91
+ assert.ok(typeof health.installedCount === 'number');
92
+ });
93
+
94
+ it('should query system extensions / drivers without crashing', async () => {
95
+ const { getSystemExtensions } = await import('./extensions.js');
96
+ const extensions = await getSystemExtensions();
97
+ assert.ok(Array.isArray(extensions));
98
+ });
99
+ });
@@ -0,0 +1,143 @@
1
+ /**
2
+ * pyre Plugin System.
3
+ *
4
+ * Discovers and executes custom user-authored metric collectors from
5
+ * ~/.config/pyre/plugins/*.js (or .mjs).
6
+ *
7
+ * Plugin contract:
8
+ * export default {
9
+ * name: 'my-metric',
10
+ * title: 'My Custom Metric',
11
+ * intervalSec: 5, // optional refresh interval
12
+ * collect: async () => {
13
+ * return {
14
+ * title: 'Custom Title', // optional override
15
+ * lines: ['Metric 1: 42', 'Status: OK']
16
+ * };
17
+ * }
18
+ * };
19
+ */
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
22
+ import { pathToFileURL } from 'node:url';
23
+ import { getConfigDir } from '../state/config.js';
24
+
25
+ export interface PyrePluginOutput {
26
+ title?: string;
27
+ lines: string[];
28
+ }
29
+
30
+ export interface PyrePlugin {
31
+ name: string;
32
+ title?: string;
33
+ intervalSec?: number;
34
+ collect: () => Promise<PyrePluginOutput | string[] | Record<string, any>>;
35
+ }
36
+
37
+ export interface LoadedPlugin {
38
+ plugin: PyrePlugin;
39
+ lastRun: number;
40
+ lastOutput: PyrePluginOutput;
41
+ error?: string;
42
+ }
43
+
44
+ const loadedPlugins: Map<string, LoadedPlugin> = new Map();
45
+
46
+ export function getPluginsDir(): string {
47
+ return path.join(getConfigDir(), 'plugins');
48
+ }
49
+
50
+ export async function loadPlugins(): Promise<Map<string, LoadedPlugin>> {
51
+ const pluginsDir = getPluginsDir();
52
+ if (!fs.existsSync(pluginsDir)) {
53
+ try {
54
+ fs.mkdirSync(pluginsDir, { recursive: true });
55
+ } catch {
56
+ return loadedPlugins;
57
+ }
58
+ }
59
+
60
+ let files: string[] = [];
61
+ try {
62
+ files = fs.readdirSync(pluginsDir).filter(f => f.endsWith('.js') || f.endsWith('.mjs'));
63
+ } catch {
64
+ return loadedPlugins;
65
+ }
66
+
67
+ for (const file of files) {
68
+ const filePath = path.join(pluginsDir, file);
69
+ try {
70
+ const fileUrl = pathToFileURL(filePath).href;
71
+ const mod = await import(`${fileUrl}?t=${Date.now()}`);
72
+ const plugin: PyrePlugin = mod.default || mod;
73
+ if (plugin && typeof plugin.collect === 'function') {
74
+ const name = plugin.name || path.basename(file, path.extname(file));
75
+ if (!loadedPlugins.has(name)) {
76
+ loadedPlugins.set(name, {
77
+ plugin: { ...plugin, name },
78
+ lastRun: 0,
79
+ lastOutput: { title: plugin.title || name, lines: ['Collecting...'] },
80
+ });
81
+ }
82
+ }
83
+ } catch (err: any) {
84
+ const name = path.basename(file, path.extname(file));
85
+ loadedPlugins.set(name, {
86
+ plugin: { name, collect: async () => ({ lines: [`Error loading plugin: ${err.message}`] }) },
87
+ lastRun: Date.now(),
88
+ lastOutput: { title: name, lines: [`Failed to load: ${err.message}`] },
89
+ error: err.message,
90
+ });
91
+ }
92
+ }
93
+
94
+ return loadedPlugins;
95
+ }
96
+
97
+ export async function executePlugins(): Promise<LoadedPlugin[]> {
98
+ const now = Date.now();
99
+ const results: LoadedPlugin[] = [];
100
+
101
+ for (const [, item] of loadedPlugins.entries()) {
102
+ const intervalMs = (item.plugin.intervalSec || 5) * 1000;
103
+ if (now - item.lastRun >= intervalMs) {
104
+ try {
105
+ const raw = await Promise.race([
106
+ item.plugin.collect(),
107
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout (5s)')), 5000)),
108
+ ]);
109
+
110
+ let output: PyrePluginOutput;
111
+ if (Array.isArray(raw)) {
112
+ output = { title: item.plugin.title || item.plugin.name, lines: raw.map(String) };
113
+ } else if (raw && typeof raw === 'object' && Array.isArray((raw as any).lines)) {
114
+ output = {
115
+ title: (raw as any).title || item.plugin.title || item.plugin.name,
116
+ lines: (raw as any).lines.map(String),
117
+ };
118
+ } else if (raw && typeof raw === 'object') {
119
+ output = {
120
+ title: item.plugin.title || item.plugin.name,
121
+ lines: Object.entries(raw).map(([k, v]) => `${k}: ${v}`),
122
+ };
123
+ } else {
124
+ output = { title: item.plugin.title || item.plugin.name, lines: [String(raw)] };
125
+ }
126
+
127
+ item.lastOutput = output;
128
+ item.lastRun = now;
129
+ item.error = undefined;
130
+ } catch (err: any) {
131
+ item.lastOutput = {
132
+ title: item.plugin.title || item.plugin.name,
133
+ lines: [`Execution error: ${err.message}`],
134
+ };
135
+ item.error = err.message;
136
+ item.lastRun = now;
137
+ }
138
+ }
139
+ results.push(item);
140
+ }
141
+
142
+ return results;
143
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * pyre profile-proc — Process CPU Profiler.
3
+ *
4
+ * Samples a process for a given duration (default 5 seconds) using macOS `sample`
5
+ * or fallback thread analysis, then presents an annotated call-tree and
6
+ * hot frames summary.
7
+ */
8
+ import { execFile } from 'node:child_process';
9
+ import { promisify } from 'node:util';
10
+ import chalk from 'chalk';
11
+
12
+ const execFileAsync = promisify(execFile);
13
+
14
+ export interface ProfileProcOptions {
15
+ pid: number;
16
+ durationSec?: number;
17
+ raw?: boolean;
18
+ }
19
+
20
+ export async function runProfileProcCommand(opts: ProfileProcOptions): Promise<void> {
21
+ const { pid, durationSec = 5, raw } = opts;
22
+
23
+ if (isNaN(pid) || pid <= 0) {
24
+ console.error(chalk.red('\n ✖ Error: Valid PID is required. Usage: pyre profile-proc <pid> [--duration <sec>]\n'));
25
+ process.exit(1);
26
+ }
27
+
28
+ // 1. Verify process exists
29
+ try {
30
+ process.kill(pid, 0);
31
+ } catch {
32
+ console.error(chalk.red(`\n ✖ Error: Process with PID ${pid} is not running or accessible.\n`));
33
+ process.exit(1);
34
+ }
35
+
36
+ console.log(chalk.bold(`\n 🔥 pyre profile-proc — Profiling PID ${pid} for ${durationSec}s...\n`));
37
+
38
+ let sampleOutput = '';
39
+ let usedFallback = false;
40
+
41
+ // Try macOS native `sample` command (available on macOS)
42
+ if (process.platform === 'darwin') {
43
+ try {
44
+ const { stdout } = await execFileAsync('sample', [String(pid), String(durationSec), '-file', '/dev/stdout'], {
45
+ timeout: (durationSec + 5) * 1000,
46
+ maxBuffer: 10 * 1024 * 1024,
47
+ });
48
+ sampleOutput = stdout;
49
+ } catch (err: any) {
50
+ // Sample might fail if SIP restricts it on system processes or timeout
51
+ sampleOutput = err.stdout || '';
52
+ }
53
+ }
54
+
55
+ // Fallback: If sample produced no usable output or running on Linux, sample via thread / proc stats
56
+ if (!sampleOutput || sampleOutput.length < 100) {
57
+ usedFallback = true;
58
+ try {
59
+ const { stdout } = await execFileAsync('ps', ['-M', '-p', String(pid), '-o', 'pid,thread,pcpu,pmem,state,time,command']);
60
+ sampleOutput = stdout;
61
+ } catch {
62
+ sampleOutput = `Process PID ${pid} active. Deep stack sampling requires elevated permissions or user-owned process.`;
63
+ }
64
+ }
65
+
66
+ if (raw) {
67
+ console.log(sampleOutput);
68
+ return;
69
+ }
70
+
71
+ // Parse and display clean call-tree analysis
72
+ if (!usedFallback) {
73
+ // Extract Call graph section
74
+ const lines = sampleOutput.split('\n');
75
+ const headerLines: string[] = [];
76
+ const stackLines: string[] = [];
77
+ let inCallGraph = false;
78
+
79
+ for (const line of lines) {
80
+ if (line.includes('Analysis of sampling') || line.includes('Process:') || line.includes('Path:') || line.includes('Identifier:')) {
81
+ headerLines.push(line.trim());
82
+ }
83
+ if (line.includes('Call graph:') || line.includes('+ Line Coverage')) {
84
+ inCallGraph = true;
85
+ continue;
86
+ }
87
+ if (inCallGraph) {
88
+ if (line.includes('Total number in stack') || line.includes('Binary Images:')) {
89
+ inCallGraph = false;
90
+ } else if (line.trim().length > 0 && stackLines.length < 35) {
91
+ stackLines.push(line);
92
+ }
93
+ }
94
+ }
95
+
96
+ if (headerLines.length > 0) {
97
+ console.log(chalk.dim(' Process Info:'));
98
+ for (const h of headerLines.slice(0, 4)) {
99
+ console.log(` ${chalk.cyan(h)}`);
100
+ }
101
+ console.log();
102
+ }
103
+
104
+ console.log(chalk.bold(' Top Call Stacks & Hot Functions:'));
105
+ console.log(chalk.dim(' ' + '─'.repeat(70)));
106
+
107
+ if (stackLines.length === 0) {
108
+ for (const l of lines.slice(0, 20)) {
109
+ if (l.trim()) console.log(` ${l}`);
110
+ }
111
+ } else {
112
+ for (const l of stackLines) {
113
+ if (l.includes('???')) {
114
+ console.log(chalk.dim(` ${l}`));
115
+ } else if (l.includes('+')) {
116
+ console.log(chalk.yellow(` ${l}`));
117
+ } else {
118
+ console.log(` ${l}`);
119
+ }
120
+ }
121
+ }
122
+ } else {
123
+ console.log(chalk.yellow(' Note: Kernel/SIP restriction prevented deep call-graph unwind.'));
124
+ console.log(chalk.dim(' Showing process thread snapshot:\n'));
125
+ for (const l of sampleOutput.split('\n')) {
126
+ if (l.trim()) console.log(` ${l}`);
127
+ }
128
+ }
129
+
130
+ console.log();
131
+ console.log(chalk.green(` ✔ Sampling complete for PID ${pid} (${durationSec}s profile duration).`));
132
+ console.log();
133
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * pyre prometheus — Prometheus metrics exporter.
3
+ *
4
+ * Starts an HTTP server that exposes system metrics in the
5
+ * Prometheus text exposition format at GET /metrics.
6
+ * Uses the existing formatPrometheusMetrics() from prometheus.ts
7
+ * and collectAll() from monitors/index.ts.
8
+ *
9
+ * No new dependencies — uses Node.js built-in http module.
10
+ */
11
+ import http from 'node:http';
12
+ import os from 'node:os';
13
+ import { collectAll } from './monitors/index.js';
14
+ import { formatPrometheusMetrics } from './prometheus.js';
15
+ import chalk from 'chalk';
16
+
17
+ export interface PrometheusOptions {
18
+ port: number;
19
+ interval: number;
20
+ detailed?: boolean;
21
+ }
22
+
23
+ export async function runPrometheusServer(opts: PrometheusOptions): Promise<void> {
24
+ const { port, interval, detailed } = opts;
25
+
26
+ // Pre-warm with an initial collection so /metrics is never empty on first scrape
27
+ let latestData = await collectAll({ detailed });
28
+ let lastCollect = Date.now();
29
+
30
+ // Background refresh loop — collects fresh data every `interval` seconds
31
+ const refreshTimer = setInterval(async () => {
32
+ try {
33
+ latestData = await collectAll({ detailed });
34
+ lastCollect = Date.now();
35
+ } catch {
36
+ // Keep serving last good data on collection failure
37
+ }
38
+ }, interval * 1000);
39
+
40
+ const server = http.createServer(async (req, res) => {
41
+ const url = req.url?.split('?')[0] ?? '/';
42
+
43
+ if (url === '/metrics') {
44
+ try {
45
+ // If data is stale (> 2x interval), re-collect synchronously
46
+ if (Date.now() - lastCollect > interval * 2000) {
47
+ latestData = await collectAll({ detailed });
48
+ lastCollect = Date.now();
49
+ }
50
+ const body = formatPrometheusMetrics(latestData);
51
+ res.writeHead(200, {
52
+ 'Content-Type': 'text/plain; version=0.0.4; charset=utf-8',
53
+ 'Content-Length': Buffer.byteLength(body),
54
+ });
55
+ res.end(body);
56
+ } catch (err) {
57
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
58
+ res.end(`# collection error: ${err instanceof Error ? err.message : String(err)}\n`);
59
+ }
60
+ return;
61
+ }
62
+
63
+ if (url === '/health' || url === '/api/health') {
64
+ const body = JSON.stringify({ status: 'ok', uptime: os.uptime(), timestamp: new Date().toISOString() });
65
+ res.writeHead(200, { 'Content-Type': 'application/json' });
66
+ res.end(body);
67
+ return;
68
+ }
69
+
70
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
71
+ res.end('Not found. Available endpoints: /metrics /health\n');
72
+ });
73
+
74
+ server.on('error', (err: NodeJS.ErrnoException) => {
75
+ if (err.code === 'EADDRINUSE') {
76
+ console.error(chalk.red(` ✖ Port ${port} is already in use. Try --prometheus-port <other-port>`));
77
+ } else {
78
+ console.error(chalk.red(` ✖ Server error: ${err.message}`));
79
+ }
80
+ clearInterval(refreshTimer);
81
+ process.exit(1);
82
+ });
83
+
84
+ await new Promise<void>((resolve) => server.listen(port, '0.0.0.0', resolve));
85
+
86
+ console.log(chalk.bold('\n 🔥 pyre — Prometheus Exporter\n'));
87
+ console.log(` ${chalk.green('✔')} Metrics endpoint: ${chalk.cyan(`http://0.0.0.0:${port}/metrics`)}`);
88
+ console.log(` ${chalk.green('✔')} Health endpoint: ${chalk.cyan(`http://0.0.0.0:${port}/health`)}`);
89
+ console.log(` ${chalk.dim(`Refresh interval: ${interval}s | Detailed mode: ${detailed ? 'on' : 'off'}`)}`);
90
+ console.log(`\n ${chalk.dim('Add to prometheus.yml:')}`);
91
+ console.log(chalk.dim(` - job_name: 'pyre'`));
92
+ console.log(chalk.dim(` static_configs:`));
93
+ console.log(chalk.dim(` - targets: ['localhost:${port}']`));
94
+ console.log(`\n ${chalk.dim('Press Ctrl+C to stop.')}\n`);
95
+
96
+ const shutdown = () => {
97
+ clearInterval(refreshTimer);
98
+ server.close(() => {
99
+ console.log(chalk.dim('\n Prometheus server stopped.'));
100
+ process.exit(0);
101
+ });
102
+ };
103
+ process.on('SIGINT', shutdown);
104
+ process.on('SIGTERM', shutdown);
105
+
106
+ await new Promise<void>(() => { /* runs until signal */ });
107
+ }