pyre-cli 9.5.2 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -16
- package/dist/alertChannels-TPSQIJ7S.js +8 -0
- package/dist/{blender-7UT5J7IP.js → blender-FGH3ASEQ.js} +2 -2
- package/dist/brewHealth-P2GW5Y5C.js +176 -0
- package/dist/buildCmd-CDPZ2ZFA.js +38 -0
- package/dist/{checkCmd-T7O4KXXA.js → checkCmd-RKW6QGDR.js} +9 -11
- package/dist/{chunk-CXL4GYGQ.js → chunk-53O2RGSL.js} +44 -17
- package/dist/{chunk-3TE7MJLS.js → chunk-6UHZP4QP.js} +36 -10
- package/dist/chunk-GHRDW646.js +2506 -0
- package/dist/{prometheus-LFYHF6VO.js → chunk-HT4VQ4OH.js} +1 -0
- package/dist/chunk-M73HCQD2.js +95 -0
- package/dist/chunk-MZO5QKUR.js +32 -0
- package/dist/{chunk-25I533OF.js → chunk-VIACN2RU.js} +1 -1
- package/dist/chunk-Z2YV5HKV.js +87 -0
- package/dist/chunk-ZLVIVFSO.js +110 -0
- package/dist/{collectors-JB4OGUEF.js → collectors-H43PN4PS.js} +6 -3
- package/dist/{completions-VQMHQCXE.js → completions-JKYEJNGR.js} +34 -1
- package/dist/{config-KS5ZZ3VN.js → config-GYGJXU2Q.js} +9 -1
- package/dist/doctor-GYHFTNZQ.js +387 -0
- package/dist/explainCmd-MF6PIMLW.js +222 -0
- package/dist/extensions-XJGHG7X3.js +134 -0
- package/dist/index.js +460 -88
- package/dist/ioreport.py +72 -0
- package/dist/netUsageCmd-WKBTW2YG.js +90 -0
- package/dist/{pipeCmd-TYLJHQ5B.js → pipeCmd-7REDBLBU.js} +4 -3
- package/dist/profileProcCmd-N4H6MCBT.js +107 -0
- package/dist/prometheus-5BL5PVLC.js +6 -0
- package/dist/prometheusCmd-PL64BLN5.js +93 -0
- package/dist/replayCmd-Z3PYXY7V.js +164 -0
- package/dist/run-HOGQDDTA.js +8 -0
- package/dist/serveCmd-2H5VLF3P.js +153 -0
- package/dist/service-Q74BBILO.js +181 -0
- package/dist/smartCmd-ODWKSFNG.js +166 -0
- package/dist/syncCmd-A2W3QB3S.js +126 -0
- package/dist/topoCmd-DU2HHZNH.js +148 -0
- package/dist/watchdogCmd-UF53JPZB.js +161 -0
- package/dist/{wizard-Z3RGA3VB.js → wizard-TD7BGX6O.js} +1 -1
- package/install.ps1 +29 -0
- package/install.sh +10 -9
- package/package.json +7 -3
- package/src/ai/models.ts +138 -0
- package/src/alertChannels.ts +119 -0
- package/src/brewHealth.ts +157 -40
- package/src/buildCmd.ts +48 -0
- package/src/checkCmd.ts +5 -5
- package/src/completions.ts +35 -0
- package/src/doctor.ts +235 -7
- package/src/easy_features.test.ts +40 -0
- package/src/explainCmd.ts +261 -0
- package/src/extensions.ts +71 -7
- package/src/formatters/render.ts +34 -10
- package/src/formatters/types.ts +2 -0
- package/src/index.ts +251 -36
- package/src/live/input.ts +60 -0
- package/src/live/render.ts +67 -0
- package/src/live/state.ts +5 -0
- package/src/live/types.ts +1 -1
- package/src/monitors/batteryPredictor.ts +143 -0
- package/src/monitors/buildTracker.ts +121 -0
- package/src/monitors/collectors.ts +342 -178
- package/src/monitors/ioreport.py +72 -0
- package/src/monitors/platform/linux.ts +756 -0
- package/src/monitors/platform/windows.ts +519 -0
- package/src/monitors/run.ts +27 -3
- package/src/monitors/smc.ts +21 -1
- package/src/monitors/types.ts +15 -0
- package/src/netUsageCmd.ts +121 -0
- package/src/platform.test.ts +99 -0
- package/src/plugins/index.ts +143 -0
- package/src/profileProcCmd.ts +133 -0
- package/src/prometheusCmd.ts +107 -0
- package/src/replayCmd.ts +186 -0
- package/src/serveCmd.ts +180 -0
- package/src/service.ts +125 -7
- package/src/smartCmd.ts +205 -0
- package/src/state/config.ts +58 -17
- package/src/syncCmd.ts +166 -0
- package/src/topoCmd.ts +176 -0
- package/src/watchdogCmd.ts +198 -0
- package/src/xbar.ts +22 -11
- package/dist/brewHealth-F2BXF5M6.js +0 -74
- package/dist/chunk-FH5WDJBF.js +0 -15
- package/dist/chunk-N4WJVF5M.js +0 -1184
- package/dist/doctor-UV7TWIHX.js +0 -184
- package/dist/extensions-DVA5YLGH.js +0 -74
- package/dist/service-WTS3NBQU.js +0 -73
package/src/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 (
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
//
|
|
28
|
-
let cellarSize = 'N/A';
|
|
104
|
+
// Try DNF
|
|
29
105
|
try {
|
|
30
|
-
const
|
|
31
|
-
if (
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
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
|
-
|
|
120
|
+
// ignore
|
|
38
121
|
}
|
|
122
|
+
}
|
|
39
123
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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(' ⚠
|
|
181
|
+
console.log(chalk.yellow(' ⚠ No supported package manager detected on PATH.\n'));
|
|
68
182
|
return;
|
|
69
183
|
}
|
|
70
184
|
|
|
71
|
-
console.log(` 📦
|
|
72
|
-
console.log(`
|
|
73
|
-
console.log(`
|
|
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(
|
|
194
|
+
console.log(chalk.green(` ✔ ${health.managerName}: Ready and healthy.`));
|
|
78
195
|
} else {
|
|
79
|
-
console.log(chalk.yellow(` ⚠
|
|
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
|
}
|
package/src/buildCmd.ts
ADDED
|
@@ -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.
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (
|
|
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
|
}
|
package/src/completions.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
|