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
package/src/brewHealth.ts CHANGED
@@ -7,76 +7,193 @@ export interface BrewHealthInfo {
7
7
  cellarSize: string;
8
8
  doctorWarnings: string[];
9
9
  brewAvailable: boolean;
10
+ managerName: string;
10
11
  }
11
12
 
12
13
  export async function getBrewHealth(): Promise<BrewHealthInfo> {
14
+ const isWindows = process.platform === 'win32';
15
+
16
+ // 1. Try Homebrew (macOS or Linuxbrew)
13
17
  try {
14
- const brewPath = await run('which brew 2>&1', '');
15
- if (!brewPath.trim() || brewPath.includes('not found')) {
16
- return { installedCount: 0, outdatedCount: 0, cellarSize: 'N/A', doctorWarnings: [], brewAvailable: false };
18
+ const brewPath = await run(isWindows ? 'where brew 2>nul' : 'which brew 2>&1', '');
19
+ if (brewPath.trim() && !brewPath.includes('not found') && !brewPath.includes('Could not find')) {
20
+ const outdatedRaw = await run('brew outdated --quiet 2>&1', '');
21
+ const outdatedLines = outdatedRaw.trim().split('\n').filter(l => l.length > 0 && !l.startsWith('Homebrew') && !l.startsWith('==>'));
22
+
23
+ const listRaw = await run('brew list --formula -1 2>&1', '');
24
+ const listLines = listRaw.trim().split('\n').filter(l => l.length > 0);
25
+
26
+ let cellarSize = 'N/A';
27
+ try {
28
+ const cellarPath = await run('brew --cellar 2>&1', '');
29
+ if (cellarPath.trim()) {
30
+ const duRaw = await run(`du -sh "${cellarPath.trim()}" 2>&1`, '');
31
+ const duMatch = duRaw.match(/^([0-9.,]+[KMGTP]?i?B?)/i);
32
+ if (duMatch) cellarSize = duMatch[1];
33
+ }
34
+ } catch {
35
+ cellarSize = 'N/A';
36
+ }
37
+
38
+ const doctorRaw = await run('brew doctor 2>&1', '');
39
+ const doctorWarnings: string[] = [];
40
+ const docLines = doctorRaw.split('\n');
41
+ for (const line of docLines) {
42
+ if (line.startsWith('Warning:')) {
43
+ doctorWarnings.push(line.replace(/^Warning:\s*/, '').trim());
44
+ }
45
+ }
46
+
47
+ return {
48
+ installedCount: listLines.length,
49
+ outdatedCount: outdatedLines.length,
50
+ cellarSize,
51
+ doctorWarnings,
52
+ brewAvailable: true,
53
+ managerName: 'Homebrew',
54
+ };
17
55
  }
56
+ } catch {
57
+ // continue to alternative managers
58
+ }
18
59
 
19
- // Outdated count without triggering brew update
20
- const outdatedRaw = await run('brew outdated --quiet 2>&1', '');
21
- const outdatedLines = outdatedRaw.trim().split('\n').filter(l => l.length > 0 && !l.startsWith('Homebrew') && !l.startsWith('==>'));
60
+ // 2. Linux Package Managers (APT, DNF, Pacman)
61
+ if (process.platform === 'linux') {
62
+ // Try APT
63
+ try {
64
+ const whichApt = (await run('which apt-get 2>/dev/null', '')).trim();
65
+ if (whichApt) {
66
+ const listRaw = (await run('dpkg-query -l 2>/dev/null', '')).trim();
67
+ const installed = listRaw ? listRaw.split('\n').filter(l => l.startsWith('ii')).length : 0;
68
+ const upgradeRaw = (await run('apt list --upgradable 2>/dev/null', '')).trim();
69
+ const outdated = upgradeRaw ? upgradeRaw.split('\n').filter(l => l.includes('/') && !l.startsWith('Listing')).length : 0;
70
+ return {
71
+ installedCount: installed,
72
+ outdatedCount: outdated,
73
+ cellarSize: '/var/cache/apt',
74
+ doctorWarnings: [],
75
+ brewAvailable: true,
76
+ managerName: 'APT (Debian/Ubuntu)',
77
+ };
78
+ }
79
+ } catch {
80
+ // ignore
81
+ }
22
82
 
23
- // Installed list count
24
- const listRaw = await run('brew list --formula -1 2>&1', '');
25
- const listLines = listRaw.trim().split('\n').filter(l => l.length > 0);
83
+ // Try Pacman
84
+ try {
85
+ const whichPacman = (await run('which pacman 2>/dev/null', '')).trim();
86
+ if (whichPacman) {
87
+ const listRaw = (await run('pacman -Q 2>/dev/null', '')).trim();
88
+ const installed = listRaw ? listRaw.split('\n').filter(Boolean).length : 0;
89
+ const upgradeRaw = (await run('checkupdates 2>/dev/null || pacman -Qu 2>/dev/null', '')).trim();
90
+ const outdated = upgradeRaw ? upgradeRaw.split('\n').filter(Boolean).length : 0;
91
+ return {
92
+ installedCount: installed,
93
+ outdatedCount: outdated,
94
+ cellarSize: '/var/cache/pacman',
95
+ doctorWarnings: [],
96
+ brewAvailable: true,
97
+ managerName: 'Pacman (Arch)',
98
+ };
99
+ }
100
+ } catch {
101
+ // ignore
102
+ }
26
103
 
27
- // Disk usage of cellar
28
- let cellarSize = 'N/A';
104
+ // Try DNF
29
105
  try {
30
- const cellarPath = await run('brew --cellar 2>&1', '');
31
- if (cellarPath.trim()) {
32
- const duRaw = await run(`du -sh "${cellarPath.trim()}" 2>&1`, '');
33
- const duMatch = duRaw.match(/^([0-9\.\,]+[KMGTP]?i?B?)/i);
34
- if (duMatch) cellarSize = duMatch[1];
106
+ const whichDnf = (await run('which dnf 2>/dev/null', '')).trim();
107
+ if (whichDnf) {
108
+ const listRaw = (await run('dnf list installed 2>/dev/null', '')).trim();
109
+ const installed = listRaw ? listRaw.split('\n').filter(Boolean).length - 1 : 0;
110
+ return {
111
+ installedCount: Math.max(0, installed),
112
+ outdatedCount: 0,
113
+ cellarSize: '/var/cache/dnf',
114
+ doctorWarnings: [],
115
+ brewAvailable: true,
116
+ managerName: 'DNF (Fedora/RHEL)',
117
+ };
35
118
  }
36
119
  } catch {
37
- cellarSize = 'N/A';
120
+ // ignore
38
121
  }
122
+ }
39
123
 
40
- // Doctor warnings
41
- const doctorRaw = await run('brew doctor 2>&1', '');
42
- const doctorWarnings: string[] = [];
43
- const docLines = doctorRaw.split('\n');
44
- for (const line of docLines) {
45
- if (line.startsWith('Warning:')) {
46
- doctorWarnings.push(line.replace(/^Warning:\s*/, '').trim());
124
+ // 3. Windows Package Managers (winget, choco, scoop)
125
+ if (isWindows) {
126
+ // Try winget
127
+ try {
128
+ const whichWinget = (await run('where winget 2>nul', '')).trim();
129
+ if (whichWinget) {
130
+ const listRaw = (await run('winget list 2>nul', '', 4000)).trim();
131
+ const installed = listRaw ? listRaw.split('\n').filter(Boolean).length - 2 : 0;
132
+ return {
133
+ installedCount: Math.max(0, installed),
134
+ outdatedCount: 0,
135
+ cellarSize: 'N/A',
136
+ doctorWarnings: [],
137
+ brewAvailable: true,
138
+ managerName: 'Windows Package Manager (winget)',
139
+ };
47
140
  }
141
+ } catch {
142
+ // ignore
48
143
  }
49
144
 
50
- return {
51
- installedCount: listLines.length,
52
- outdatedCount: outdatedLines.length,
53
- cellarSize,
54
- doctorWarnings,
55
- brewAvailable: true,
56
- };
57
- } catch (err) {
58
- return { installedCount: 0, outdatedCount: 0, cellarSize: 'N/A', doctorWarnings: [], brewAvailable: false };
145
+ // Try scoop
146
+ try {
147
+ const whichScoop = (await run('where scoop 2>nul', '')).trim();
148
+ if (whichScoop) {
149
+ const listRaw = (await run('scoop list 2>nul', '')).trim();
150
+ const installed = listRaw ? listRaw.split('\n').filter(Boolean).length - 4 : 0;
151
+ return {
152
+ installedCount: Math.max(0, installed),
153
+ outdatedCount: 0,
154
+ cellarSize: 'N/A',
155
+ doctorWarnings: [],
156
+ brewAvailable: true,
157
+ managerName: 'Scoop',
158
+ };
159
+ }
160
+ } catch {
161
+ // ignore
162
+ }
59
163
  }
164
+
165
+ return {
166
+ installedCount: 0,
167
+ outdatedCount: 0,
168
+ cellarSize: 'N/A',
169
+ doctorWarnings: [],
170
+ brewAvailable: false,
171
+ managerName: 'None',
172
+ };
60
173
  }
61
174
 
62
175
  export async function printBrewHealthReport(): Promise<void> {
63
- console.log(chalk.bold('\n pyre brew — Homebrew Health Summary\n'));
64
176
  const health = await getBrewHealth();
177
+ const label = health.brewAvailable ? health.managerName : 'Package Manager';
178
+ console.log(chalk.bold(`\n pyre brew — ${label} Health Summary\n`));
65
179
 
66
180
  if (!health.brewAvailable) {
67
- console.log(chalk.yellow(' ⚠ Homebrew is not installed or not available on PATH.\n'));
181
+ console.log(chalk.yellow(' ⚠ No supported package manager detected on PATH.\n'));
68
182
  return;
69
183
  }
70
184
 
71
- console.log(` 📦 Installed Formulae: ${chalk.bold(health.installedCount)}`);
72
- console.log(` ⏳ Outdated Packages: ${health.outdatedCount > 0 ? chalk.yellow.bold(health.outdatedCount) : chalk.green('0 (Up to date)')}`);
73
- console.log(` 💾 Cellar Disk Usage: ${chalk.bold(health.cellarSize)}`);
185
+ console.log(` 📦 Package Manager: ${chalk.cyan.bold(health.managerName)}`);
186
+ console.log(` 📦 Installed Packages: ${chalk.bold(health.installedCount)}`);
187
+ console.log(` ⏳ Outdated Packages: ${health.outdatedCount > 0 ? chalk.yellow.bold(health.outdatedCount) : chalk.green('0 (Up to date)')}`);
188
+ if (health.cellarSize !== 'N/A') {
189
+ console.log(` 💾 Cache / Cellar: ${chalk.bold(health.cellarSize)}`);
190
+ }
74
191
 
75
192
  console.log();
76
193
  if (health.doctorWarnings.length === 0) {
77
- console.log(chalk.green(' ✔ brew doctor: No warnings detected system-wide.'));
194
+ console.log(chalk.green(` ✔ ${health.managerName}: Ready and healthy.`));
78
195
  } else {
79
- console.log(chalk.yellow(` ⚠ brew doctor warnings (${health.doctorWarnings.length}):`));
196
+ console.log(chalk.yellow(` ⚠ Diagnostics warnings (${health.doctorWarnings.length}):`));
80
197
  for (const warn of health.doctorWarnings.slice(0, 5)) {
81
198
  console.log(chalk.dim(` • ${warn}`));
82
199
  }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * pyre build — Active Build System Tracker.
3
+ *
4
+ * Discovers running compilation and build tools (xcodebuild, cargo, swift,
5
+ * make, ninja, bazel, go), displaying real-time compiler workers, parallelism,
6
+ * and CPU/RAM footprints.
7
+ */
8
+ import chalk from 'chalk';
9
+ import { collectBuilds } from './monitors/buildTracker.js';
10
+
11
+ export interface BuildOptions {
12
+ watch?: boolean;
13
+ json?: boolean;
14
+ }
15
+
16
+ export async function runBuildCommand(opts: BuildOptions = {}): Promise<void> {
17
+ const builds = await collectBuilds();
18
+
19
+ if (opts.json) {
20
+ console.log(JSON.stringify({ activeBuilds: builds }, null, 2));
21
+ return;
22
+ }
23
+
24
+ console.log(chalk.bold('\n 🔥 pyre build — Active Build System Tracker\n'));
25
+
26
+ if (builds.length === 0) {
27
+ console.log(chalk.dim(' No active build or compilation processes detected.'));
28
+ console.log(chalk.dim(' (Supports: xcodebuild, cargo, swift build, make, ninja, bazel, go build)\n'));
29
+ return;
30
+ }
31
+
32
+ for (const b of builds) {
33
+ const elapsedMins = Math.floor(b.elapsedSec / 60);
34
+ const elapsedSecs = b.elapsedSec % 60;
35
+ const timeStr = elapsedMins > 0 ? `${elapsedMins}m ${elapsedSecs}s` : `${elapsedSecs}s`;
36
+
37
+ console.log(` ${chalk.bgCyan.black.bold(` ${b.tool.toUpperCase()} `)} ${chalk.bold(`PID ${b.rootPid}`)}`);
38
+ console.log(` Command: ${chalk.white(b.command)}`);
39
+ if (b.target) {
40
+ console.log(` Target: ${chalk.cyan(b.target)}`);
41
+ }
42
+ console.log(` Compiler Workers: ${chalk.green.bold(String(b.compilerProcesses))} active thread/processes`);
43
+ console.log(` Combined CPU: ${chalk.yellow.bold(b.totalCpu.toFixed(1) + '%')}`);
44
+ console.log(` Combined RAM: ${chalk.magenta(b.totalMem.toFixed(1) + '%')}`);
45
+ console.log(` Elapsed Time: ${chalk.dim(timeStr)}`);
46
+ console.log();
47
+ }
48
+ }
package/src/checkCmd.ts CHANGED
@@ -15,7 +15,7 @@ export async function runCheckCommand(opts: CheckOptions = {}): Promise<void> {
15
15
  const cpuUsage = data.cpu.usage;
16
16
  const tempC = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die;
17
17
  const memUsage = data.memory.usagePercent;
18
- const disk = data.disk.mainVolume;
18
+ const disk = data.disk && data.disk.length > 0 ? data.disk[0] : null;
19
19
 
20
20
  const sentences: string[] = [];
21
21
  let isWarning = false;
@@ -61,10 +61,10 @@ export async function runCheckCommand(opts: CheckOptions = {}): Promise<void> {
61
61
 
62
62
  // Disk sentence
63
63
  if (disk) {
64
- const freeBytes = disk.free;
65
- const freeStr = formatBytes(freeBytes);
66
- sentences.push(`${freeStr} free on ${disk.mount}.`);
67
- if (disk.usePercent >= 90) {
64
+ const freeStr = disk.available;
65
+ sentences.push(`${freeStr} free on ${disk.mountpoint}.`);
66
+ const cap = parseInt(disk.capacity, 10);
67
+ if (!isNaN(cap) && cap >= 90) {
68
68
  isWarning = true;
69
69
  }
70
70
  }
@@ -116,3 +116,38 @@ complete -c pyre -l sort -x -a "cpu mem pid user command state threads runtime"
116
116
  complete -c pyre -l temp-unit -x -a "c f" -d "Temperature unit"
117
117
  `;
118
118
  }
119
+
120
+ export function generatePowerShellCompletions(): string {
121
+ return `# PowerShell completion for pyre
122
+ Register-ArgumentCompleter -Native -CommandName pyre -ScriptBlock {
123
+ param($wordToComplete, $commandAst, $cursorPosition)
124
+ $commands = @('live', 'check', 'pipe', 'stress', 'ui', 'web', 'ssh', 'fleet', 'bench', 'benchmark', 'anomalies', 'doctor', 'extensions', 'brew', 'update', 'profile', 'config', 'history', 'diff', 'info', 'completions', 'p2p', 'blender', 'prometheus', 'smart', 'replay', 'explain')
125
+ $options = @('--json', '--html', '--md', '--csv', '--tsv', '--detailed', '--theme', '--interval', '--once', '--out', '--export-dir', '--log', '--tree', '--sort', '--packets', '--limit', '--alert-cpu', '--alert-temp', '--temp-unit', '--plain', '--help', '--version')
126
+
127
+ $elements = $commandAst.CommandElements
128
+ if ($elements.Count -le 2) {
129
+ $commands | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
130
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
131
+ }
132
+ }
133
+ $options | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
134
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_)
135
+ }
136
+ }
137
+ `;
138
+ }
139
+
140
+ export function printCompletions(shell: string): void {
141
+ const s = (shell || '').toLowerCase();
142
+ if (s === 'bash') {
143
+ console.log(generateBashCompletions());
144
+ } else if (s === 'fish') {
145
+ console.log(generateFishCompletions());
146
+ } else if (s === 'powershell' || s === 'pwsh') {
147
+ console.log(generatePowerShellCompletions());
148
+ } else {
149
+ console.log(generateZshCompletions());
150
+ }
151
+ }
152
+
153
+
package/src/doctor.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
1
3
  import chalk from 'chalk';
2
4
  import { run } from './monitors/run.js';
3
5
  import { readConfig, CONFIG_FILE } from './state/config.js';
@@ -10,6 +12,13 @@ export interface DoctorCheck {
10
12
  }
11
13
 
12
14
  export async function runDoctor(): Promise<DoctorCheck[]> {
15
+ const platform = process.platform;
16
+ if (platform === 'linux') return runLinuxDoctor();
17
+ if (platform === 'win32') return runWindowsDoctor();
18
+ return runMacDoctor();
19
+ }
20
+
21
+ async function runMacDoctor(): Promise<DoctorCheck[]> {
13
22
  const checks: DoctorCheck[] = [];
14
23
 
15
24
  // 1. Sudo / Powermetrics Access
@@ -163,27 +172,246 @@ export async function runDoctor(): Promise<DoctorCheck[]> {
163
172
  }
164
173
 
165
174
  // 7. Config file integrity
175
+ checks.push(checkConfigIntegrity());
176
+
177
+ return checks;
178
+ }
179
+
180
+ async function runLinuxDoctor(): Promise<DoctorCheck[]> {
181
+ const checks: DoctorCheck[] = [];
182
+
183
+ // 1. Process Visibility
184
+ try {
185
+ const psCheck = await run('ps -eo pid,user,comm', '');
186
+ if (psCheck.length > 0) {
187
+ checks.push({
188
+ name: 'Process Visibility',
189
+ status: 'ok',
190
+ message: 'Full process table access available.',
191
+ });
192
+ } else {
193
+ checks.push({
194
+ name: 'Process Visibility',
195
+ status: 'warn',
196
+ message: 'Restricted process table access.',
197
+ });
198
+ }
199
+ } catch {
200
+ checks.push({
201
+ name: 'Process Visibility',
202
+ status: 'error',
203
+ message: 'Unable to query process list.',
204
+ });
205
+ }
206
+
207
+ // 2. /proc and /sys Telemetry Access
208
+ try {
209
+ const statExists = fs.existsSync('/proc/stat');
210
+ const memExists = fs.existsSync('/proc/meminfo');
211
+ if (statExists && memExists) {
212
+ checks.push({
213
+ name: 'Kernel Telemetry (/proc)',
214
+ status: 'ok',
215
+ message: 'Kernel telemetry interfaces readable.',
216
+ });
217
+ } else {
218
+ checks.push({
219
+ name: 'Kernel Telemetry (/proc)',
220
+ status: 'error',
221
+ message: 'Core pseudo-filesystems (/proc) unavailable or restricted.',
222
+ });
223
+ }
224
+ } catch {
225
+ checks.push({
226
+ name: 'Kernel Telemetry (/proc)',
227
+ status: 'warn',
228
+ message: 'Could not check /proc readability.',
229
+ });
230
+ }
231
+
232
+ // 3. Thermal & Hardware Sensors
233
+ try {
234
+ const thermZones = fs.existsSync('/sys/class/thermal');
235
+ const hwmon = fs.existsSync('/sys/class/hwmon');
236
+ if (thermZones || hwmon) {
237
+ checks.push({
238
+ name: 'Hardware Sensors (/sys)',
239
+ status: 'ok',
240
+ message: 'Thermal and hardware monitoring sensors detected.',
241
+ });
242
+ } else {
243
+ checks.push({
244
+ name: 'Hardware Sensors (/sys)',
245
+ status: 'warn',
246
+ message: 'No hardware sensor paths detected in /sys/class.',
247
+ });
248
+ }
249
+ } catch {
250
+ checks.push({
251
+ name: 'Hardware Sensors (/sys)',
252
+ status: 'warn',
253
+ message: 'Unable to query sensor interfaces.',
254
+ });
255
+ }
256
+
257
+ // 4. Network Reachability
258
+ try {
259
+ const netCheck = await run('ip route 2>/dev/null || route -n 2>/dev/null', '');
260
+ if (netCheck.includes('default') || netCheck.includes('0.0.0.0')) {
261
+ checks.push({
262
+ name: 'Network Reachability',
263
+ status: 'ok',
264
+ message: 'Active default route detected.',
265
+ });
266
+ } else {
267
+ checks.push({
268
+ name: 'Network Reachability',
269
+ status: 'warn',
270
+ message: 'No default network gateway detected.',
271
+ });
272
+ }
273
+ } catch {
274
+ checks.push({
275
+ name: 'Network Reachability',
276
+ status: 'warn',
277
+ message: 'Could not verify network routes.',
278
+ });
279
+ }
280
+
281
+ // 5. Systemd User Service Capability
282
+ try {
283
+ const sysctlRes = await run('systemctl --user status 2>&1', '');
284
+ if (!sysctlRes.includes('Failed to connect to bus') && !sysctlRes.includes('not found')) {
285
+ checks.push({
286
+ name: 'Systemd User Daemon',
287
+ status: 'ok',
288
+ message: 'Systemd user session active (ready for pyre --install).',
289
+ });
290
+ } else {
291
+ checks.push({
292
+ name: 'Systemd User Daemon',
293
+ status: 'warn',
294
+ message: 'Systemd user session not available.',
295
+ details: 'Background services via pyre --install require an active user systemd session.',
296
+ });
297
+ }
298
+ } catch {
299
+ checks.push({
300
+ name: 'Systemd User Daemon',
301
+ status: 'warn',
302
+ message: 'Systemd check skipped.',
303
+ });
304
+ }
305
+
306
+ // 6. Config file integrity
307
+ checks.push(checkConfigIntegrity());
308
+
309
+ return checks;
310
+ }
311
+
312
+ async function runWindowsDoctor(): Promise<DoctorCheck[]> {
313
+ const checks: DoctorCheck[] = [];
314
+
315
+ // 1. Process Visibility
166
316
  try {
167
- const cfg = readConfig();
317
+ const psCheck = await run('tasklist /nh 2>nul', '');
318
+ if (psCheck.length > 0) {
319
+ checks.push({
320
+ name: 'Process Visibility',
321
+ status: 'ok',
322
+ message: 'Windows process table access available.',
323
+ });
324
+ } else {
325
+ checks.push({
326
+ name: 'Process Visibility',
327
+ status: 'warn',
328
+ message: 'Tasklist query returned no output.',
329
+ });
330
+ }
331
+ } catch {
168
332
  checks.push({
333
+ name: 'Process Visibility',
334
+ status: 'error',
335
+ message: 'Unable to query Windows tasklist.',
336
+ });
337
+ }
338
+
339
+ // 2. PowerShell / WMI Interface
340
+ try {
341
+ const wmiCheck = await run('wmic os get Caption 2>nul || powershell -Command "(Get-CimInstance Win32_OperatingSystem).Caption"', '');
342
+ if (wmiCheck.toLowerCase().includes('windows')) {
343
+ checks.push({
344
+ name: 'WMI / CIM Subsystem',
345
+ status: 'ok',
346
+ message: 'WMI/CIM management queries responsive.',
347
+ });
348
+ } else {
349
+ checks.push({
350
+ name: 'WMI / CIM Subsystem',
351
+ status: 'warn',
352
+ message: 'WMI/CIM query did not return expected response.',
353
+ details: 'Some hardware metrics (disks, thermal) rely on WMI or PowerShell CIM.',
354
+ });
355
+ }
356
+ } catch {
357
+ checks.push({
358
+ name: 'WMI / CIM Subsystem',
359
+ status: 'warn',
360
+ message: 'Could not execute WMI query.',
361
+ });
362
+ }
363
+
364
+ // 3. Network Reachability
365
+ try {
366
+ const netCheck = await run('netstat -rn 2>nul', '');
367
+ if (netCheck.includes('0.0.0.0')) {
368
+ checks.push({
369
+ name: 'Network Reachability',
370
+ status: 'ok',
371
+ message: 'Active default IPv4 gateway detected.',
372
+ });
373
+ } else {
374
+ checks.push({
375
+ name: 'Network Reachability',
376
+ status: 'warn',
377
+ message: 'No default route detected in routing table.',
378
+ });
379
+ }
380
+ } catch {
381
+ checks.push({
382
+ name: 'Network Reachability',
383
+ status: 'warn',
384
+ message: 'Could not verify Windows network routes.',
385
+ });
386
+ }
387
+
388
+ // 4. Config file integrity
389
+ checks.push(checkConfigIntegrity());
390
+
391
+ return checks;
392
+ }
393
+
394
+ function checkConfigIntegrity(): DoctorCheck {
395
+ try {
396
+ readConfig();
397
+ return {
169
398
  name: 'Configuration File',
170
399
  status: 'ok',
171
400
  message: `Config file valid (${CONFIG_FILE}).`,
172
- });
401
+ };
173
402
  } catch (err: any) {
174
- checks.push({
403
+ return {
175
404
  name: 'Configuration File',
176
405
  status: 'error',
177
406
  message: 'Failed to parse config file.',
178
407
  details: err.message,
179
- });
408
+ };
180
409
  }
181
-
182
- return checks;
183
410
  }
184
411
 
185
412
  export function printDoctorReport(checks: DoctorCheck[]): void {
186
- console.log(chalk.bold('\n pyre doctor — System Diagnostics\n'));
413
+ const osLabel = process.platform === 'darwin' ? 'macOS' : (process.platform === 'win32' ? 'Windows' : 'Linux');
414
+ console.log(chalk.bold(`\n pyre doctor — System Diagnostics (${osLabel})\n`));
187
415
 
188
416
  for (const check of checks) {
189
417
  let icon = chalk.green('✔');
@@ -1,5 +1,10 @@
1
1
  import { describe, it } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
+ import path from 'node:path';
4
+ import fs from 'node:fs';
5
+
6
+ process.env.PYRE_CONFIG_DIR = path.join(process.cwd(), 'node_modules', '.test-pyre-config');
7
+
3
8
  import { parseSystemExtensionsOutput } from './extensions.js';
4
9
  import { saveProfile, loadProfile, listProfiles } from './state/config.js';
5
10
  import { sparkline } from './sparkline.js';
@@ -41,3 +46,38 @@ describe('Plain text sparkline (Feature F3)', () => {
41
46
  assert.strictEqual(res, '[10.0 -> 30.0 (Δ:+20.0)]');
42
47
  });
43
48
  });
49
+
50
+ describe('Cross-Platform Support', () => {
51
+ it('should generate valid PowerShell completions script', async () => {
52
+ const { generatePowerShellCompletions } = await import('./completions.js');
53
+ const ps = generatePowerShellCompletions();
54
+ assert.ok(ps.includes('Register-ArgumentCompleter'));
55
+ assert.ok(ps.includes('CommandName pyre'));
56
+ assert.ok(ps.includes('live'));
57
+ assert.ok(ps.includes('--json'));
58
+ });
59
+
60
+ it('should run doctor diagnostics without crashing on any platform', async () => {
61
+ const { runDoctor } = await import('./doctor.js');
62
+ const checks = await runDoctor();
63
+ assert.ok(Array.isArray(checks));
64
+ assert.ok(checks.length > 0);
65
+ for (const c of checks) {
66
+ assert.ok(['ok', 'warn', 'error'].includes(c.status));
67
+ assert.ok(typeof c.name === 'string');
68
+ assert.ok(typeof c.message === 'string');
69
+ }
70
+ });
71
+
72
+ it('should collect telemetry across subsystems without crashing', async () => {
73
+ const { collectAll } = await import('./monitors/index.js');
74
+ const data = await collectAll();
75
+ assert.ok(data.header.hostname.length > 0);
76
+ assert.ok(data.header.os.length > 0);
77
+ assert.ok(data.cpu.cores >= 1);
78
+ assert.ok(data.memory.total > 0);
79
+ assert.ok(Array.isArray(data.disk));
80
+ assert.ok(typeof data.network.interface === 'string');
81
+ });
82
+ });
83
+