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.
Files changed (94) hide show
  1. package/README.md +84 -14
  2. package/dist/alertChannels-TPSQIJ7S.js +8 -0
  3. package/dist/blender-FGH3ASEQ.js +7 -0
  4. package/dist/brewHealth-P2GW5Y5C.js +176 -0
  5. package/dist/buildCmd-CDPZ2ZFA.js +38 -0
  6. package/dist/{checkCmd-EWFNLBWE.js → checkCmd-RKW6QGDR.js} +10 -11
  7. package/dist/{chunk-RVGKEJXT.js → chunk-53O2RGSL.js} +47 -17
  8. package/dist/{chunk-MOAEGMFP.js → chunk-6UHZP4QP.js} +204 -46
  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-VIACN2RU.js +133 -0
  14. package/dist/chunk-Z2YV5HKV.js +87 -0
  15. package/dist/chunk-ZLVIVFSO.js +110 -0
  16. package/dist/{collectors-WD6VKH6M.js → collectors-H43PN4PS.js} +8 -2
  17. package/dist/{completions-VQMHQCXE.js → completions-JKYEJNGR.js} +34 -1
  18. package/dist/{config-4ZLF4HTS.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 +841 -206
  23. package/dist/ioreport.py +72 -0
  24. package/dist/netUsageCmd-WKBTW2YG.js +90 -0
  25. package/dist/{pipeCmd-7THXEGDH.js → pipeCmd-7REDBLBU.js} +5 -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-GVIAHIVV.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/anomalies.ts +114 -60
  44. package/src/brewHealth.ts +157 -40
  45. package/src/buildCmd.ts +48 -0
  46. package/src/checkCmd.ts +5 -5
  47. package/src/completions.ts +35 -0
  48. package/src/doctor.ts +235 -7
  49. package/src/easy_features.test.ts +40 -0
  50. package/src/explainCmd.ts +261 -0
  51. package/src/extensions.ts +71 -7
  52. package/src/formatters/index.ts +1 -1
  53. package/src/formatters/output.ts +100 -13
  54. package/src/formatters/render.ts +274 -38
  55. package/src/formatters/themes.ts +8 -0
  56. package/src/formatters/types.ts +6 -1
  57. package/src/index.ts +298 -56
  58. package/src/live/input.ts +122 -4
  59. package/src/live/render.ts +98 -0
  60. package/src/live/state.ts +70 -52
  61. package/src/live/types.ts +2 -2
  62. package/src/monitors/batteryPredictor.ts +143 -0
  63. package/src/monitors/blender.ts +144 -0
  64. package/src/monitors/buildTracker.ts +121 -0
  65. package/src/monitors/collectors.ts +558 -147
  66. package/src/monitors/dns_cache.ts +48 -0
  67. package/src/monitors/index.ts +3 -2
  68. package/src/monitors/ioreport.py +72 -0
  69. package/src/monitors/platform/linux.ts +756 -0
  70. package/src/monitors/platform/windows.ts +519 -0
  71. package/src/monitors/run.ts +27 -3
  72. package/src/monitors/smc.ts +21 -1
  73. package/src/monitors/types.ts +112 -14
  74. package/src/netUsageCmd.ts +121 -0
  75. package/src/platform.test.ts +99 -0
  76. package/src/plugins/index.ts +143 -0
  77. package/src/profileProcCmd.ts +133 -0
  78. package/src/prometheusCmd.ts +107 -0
  79. package/src/replayCmd.ts +186 -0
  80. package/src/serveCmd.ts +180 -0
  81. package/src/service.ts +125 -7
  82. package/src/smartCmd.ts +205 -0
  83. package/src/state/config.ts +64 -17
  84. package/src/syncCmd.ts +166 -0
  85. package/src/topoCmd.ts +176 -0
  86. package/src/watchdogCmd.ts +198 -0
  87. package/src/xbar.ts +22 -11
  88. package/dist/brewHealth-F2BXF5M6.js +0 -74
  89. package/dist/chunk-FH5WDJBF.js +0 -15
  90. package/dist/chunk-HGRBBPYX.js +0 -937
  91. package/dist/doctor-L6FZYA7F.js +0 -184
  92. package/dist/extensions-DVA5YLGH.js +0 -74
  93. package/dist/service-WTS3NBQU.js +0 -73
  94. /package/dist/{chunk-SG26Y3WF.js → chunk-IM3IQMMT.js} +0 -0
@@ -21,6 +21,20 @@ export interface StatsData {
21
21
  timestamp: string;
22
22
  packets: PacketData | null;
23
23
  tasks: TaskData[];
24
+ containers?: ContainerData[];
25
+ blenderRenders?: BlenderRenderData[];
26
+ activeBuilds?: import('./buildTracker.js').ActiveBuild[];
27
+ }
28
+
29
+ export interface ContainerData {
30
+ id: string;
31
+ name: string;
32
+ cpuPercent: number;
33
+ memUsage: string;
34
+ memPercent: number;
35
+ netIO: string;
36
+ blockIO: string;
37
+ pids: number;
24
38
  }
25
39
 
26
40
  export interface GpuData {
@@ -38,18 +52,20 @@ export interface GpuData {
38
52
  combinedWatts?: number;
39
53
  }
40
54
 
41
- export interface PacketData {
42
- totalPackets: number;
43
- rxPackets: number;
44
- txPackets: number;
45
- rxRate: number;
46
- txRate: number;
47
- connections: number;
48
- topProcesses: { pid: number; command: string; rxBytes: number; txBytes: number }[];
49
- /** Every process with an open network connection (unlike topProcesses, not truncated). */
50
- allProcesses: { pid: number; command: string; rxBytes: number; txBytes: number; rxPackets?: number; txPackets?: number }[];
51
- interfaces: { iface: string; rxPackets: number; txPackets: number; rxBytes: number; txBytes: number }[];
52
- }
55
+ export interface PacketData {
56
+ totalPackets: number;
57
+ rxPackets: number;
58
+ txPackets: number;
59
+ rxRate: number;
60
+ txRate: number;
61
+ connections: number;
62
+ topProcesses: NetworkProcess[];
63
+ allProcesses: NetworkProcess[];
64
+ interfaces: { iface: string; rxPackets: number; txPackets: number; rxBytes: number; txBytes: number }[];
65
+ protocolStats?: ProtocolStats;
66
+ connectionStates?: ConnectionStateStats;
67
+ listeningPorts?: number[];
68
+ }
53
69
 
54
70
  export interface TaskData {
55
71
  pid: number;
@@ -96,12 +112,25 @@ export interface MemoryData {
96
112
  pressureLevel?: string;
97
113
  }
98
114
 
115
+ export interface FanData {
116
+ /** Fan index (0-based) */
117
+ id: number;
118
+ /** Current fan speed in RPM */
119
+ rpm: number;
120
+ /** Minimum fan speed in RPM (from SMC), when available */
121
+ minRpm?: number;
122
+ /** Maximum fan speed in RPM (from SMC), when available */
123
+ maxRpm?: number;
124
+ }
125
+
99
126
  export interface ThermalData {
100
127
  state: string;
101
128
  detail?: string;
102
129
  /** Normalized 0-3 scale (nominal/fair/serious/critical) derived from `state`, for graphing/coloring */
103
130
  pressureLevel: number;
104
131
  temperatures?: Record<string, number | null>;
132
+ /** Fan speeds from SMC (empty array when not available or no sudo) */
133
+ fans?: FanData[];
105
134
  error?: string;
106
135
  }
107
136
 
@@ -118,6 +147,7 @@ export interface BatteryData {
118
147
  dischargeRatePerHour?: number;
119
148
  powerWatts?: number;
120
149
  friendlySummary?: string;
150
+ smartPrediction?: import('./batteryPredictor.js').SmartBatteryPrediction;
121
151
  }
122
152
 
123
153
  export interface DiskData {
@@ -138,10 +168,60 @@ export interface NetworkData {
138
168
  txBytes: number;
139
169
  rxPackets: number;
140
170
  txPackets: number;
171
+ rxRate: number;
172
+ txRate: number;
141
173
  rxPacketsPerSec?: number;
142
174
  txPacketsPerSec?: number;
143
175
  connections?: number;
144
176
  topProcesses?: NetworkProcess[];
177
+ protocols?: ProtocolStats;
178
+ connectionStates?: ConnectionStateStats;
179
+ listeningPorts?: number[];
180
+ establishedConnections?: number;
181
+ topRemoteHosts?: RemoteHostInfo[];
182
+ }
183
+
184
+ export interface ProtocolStats {
185
+ tcp: { connections: number; rxBytes: number; txBytes: number };
186
+ udp: { connections: number; rxBytes: number; txBytes: number };
187
+ }
188
+
189
+ export interface ConnectionStateStats {
190
+ established: number;
191
+ listening: number;
192
+ timeWait: number;
193
+ closeWait: number;
194
+ other: number;
195
+ }
196
+
197
+ export interface RemoteHostInfo {
198
+ host: string;
199
+ ip: string;
200
+ country?: string;
201
+ city?: string;
202
+ asn?: string;
203
+ bytesTransferred: number;
204
+ connectionCount: number;
205
+ }
206
+
207
+ export interface DnsQuery {
208
+ timestamp: number;
209
+ domain: string;
210
+ type: string;
211
+ process: string;
212
+ }
213
+
214
+ export interface NetworkConnection {
215
+ protocol: string;
216
+ local: string;
217
+ remote: string;
218
+ state: string;
219
+ rxBytes: number;
220
+ txBytes: number;
221
+ remoteHost?: string;
222
+ country?: string;
223
+ city?: string;
224
+ asn?: string;
145
225
  }
146
226
 
147
227
  export interface NetworkProcess {
@@ -149,8 +229,11 @@ export interface NetworkProcess {
149
229
  command: string;
150
230
  rxBytes: number;
151
231
  txBytes: number;
152
- rxPackets: number;
153
- txPackets: number;
232
+ rxRate?: number;
233
+ txRate?: number;
234
+ rxPackets?: number;
235
+ txPackets?: number;
236
+ connections?: NetworkConnection[];
154
237
  }
155
238
 
156
239
  export interface ProcessData {
@@ -164,4 +247,19 @@ export interface ProcessData {
164
247
  threads: number;
165
248
  runtime: number;
166
249
  isGpuOrMlAttributed?: boolean;
250
+ }
251
+
252
+ export interface BlenderRenderData {
253
+ pid: number;
254
+ blendFile: string;
255
+ renderEngine: string;
256
+ frameStart: number;
257
+ frameEnd: number;
258
+ currentFrame: number;
259
+ totalFrames: number;
260
+ elapsedSec: number;
261
+ completionPercent: number;
262
+ status: 'rendering' | 'finalizing' | 'loading' | 'idle' | 'unknown';
263
+ outputPath: string;
264
+ sampleCount?: number;
167
265
  }
@@ -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
+ }